1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+import functools
4
+
5
+import torch
6
+import torch.nn as nn
7
+from transformers import PretrainedConfig
8
+
9
+from vllm import envs
10
+from vllm.config.lora import LoRAConfig
11
+from vllm.distributed.parallel_state import (
12
+ get_tensor_model_parallel_rank,
13
+ get_tensor_model_parallel_world_size,
14
+)
15
+from vllm.lora.layers.base import BaseLayerWithLoRA
16
+from vllm.model_executor.layers.fused_moe import FusedMoE
17
+from vllm.model_executor.layers.fused_moe.config import (
18
+ FUSED_MOE_UNQUANTIZED_CONFIG,
19
+ _get_config_dtype_str,
20
+ mxfp4_w4a16_moe_quant_config,
21
+)
22
+from vllm.model_executor.layers.fused_moe.fused_marlin_moe import (
23
+ modular_marlin_fused_moe,
24
+)
25
+from vllm.model_executor.layers.fused_moe.fused_moe import (
26
+ modular_triton_fused_moe,
27
+ try_get_optimal_moe_config,
28
+)
29
+from vllm.model_executor.layers.quantization.mxfp4 import Mxfp4Config
30
+
31
+
32
+class FusedMoEWithLoRA(BaseLayerWithLoRA):
33
+ def __init__(self, base_layer: FusedMoE) -> None:
34
+ super().__init__()
35
+ self.base_layer = base_layer
36
+ self.tp_size = get_tensor_model_parallel_world_size()
37
+ self.tp_rank = get_tensor_model_parallel_rank()
38
+ self.device = base_layer.w2_weight.device
39
+ self._inject_lora_into_fused_moe()
40
+
41
+ def _inject_lora_into_fused_moe(self):
42
+ moe_state_dict = {}
43
+ top_k = self.base_layer.top_k
44
+
45
+ if self.base_layer.quant_config is None:
46
+ quant_config = FUSED_MOE_UNQUANTIZED_CONFIG
47
+ elif not isinstance(self.base_layer.quant_config, Mxfp4Config):
48
+ quant_config = self.base_layer.quant_config
49
+ else:
50
+ quant_config = mxfp4_w4a16_moe_quant_config(
51
+ w1_bias=self.base_layer.w13_bias,
52
+ w2_bias=self.base_layer.w2_bias,
53
+ w1_scale=self.base_layer.w13_weight_scale,
54
+ w2_scale=self.base_layer.w2_weight_scale,
55
+ )
56
+
57
+ m_fused_moe_fn = (
58
+ modular_triton_fused_moe(
59
+ quant_config, shared_experts=self.base_layer.shared_experts
60
+ )
61
+ if not quant_config.use_mxfp4_w4a16
62
+ else modular_marlin_fused_moe(
63
+ quant_config, shared_experts=self.base_layer.shared_experts
64
+ )
65
+ )
66
+
67
+ def fwd_decorator(layer, func):
68
+ def wrapper(*args, **kwargs):
69
+ moe_state_dict["hidden_states"] = kwargs["hidden_states"]
70
+ moe_state_dict["topk_ids"] = kwargs["topk_ids"]
71
+ moe_state_dict["topk_weights"] = kwargs["topk_weights"]
72
+ moe_state_dict["global_num_experts"] = kwargs["global_num_experts"]
73
+ moe_state_dict["expert_map"] = kwargs["expert_map"]
74
+ moe_state_dict["apply_router_weight_on_input"] = kwargs[
75
+ "apply_router_weight_on_input"
76
+ ]
77
+ moe_state_dict["max_loras"] = layer.w1_lora_a_stacked.shape[0]
78
+ result = func(*args, **kwargs)
79
+ return result
80
+
81
+ return wrapper
82
+
83
+ def act_decorator(layer, func):
84
+ def wrapper(*args, **kwargs):
85
+ _, output, input = args
86
+
87
+ hidden_states = moe_state_dict["hidden_states"]
88
+ topk_weights = moe_state_dict["topk_weights"]
89
+ curr_topk_ids = moe_state_dict["topk_ids"]
90
+ global_num_experts = moe_state_dict["global_num_experts"]
91
+ expert_map = moe_state_dict["expert_map"]
92
+ max_loras = moe_state_dict["max_loras"]
93
+
94
+ config_dtype = _get_config_dtype_str(
95
+ dtype=hidden_states.dtype,
96
+ use_fp8_w8a8=False,
97
+ use_int8_w8a16=False,
98
+ use_int4_w4a16=False,
99
+ )
100
+ CHUNK_SIZE = envs.VLLM_FUSED_MOE_CHUNK_SIZE
101
+ num_tokens = hidden_states.size(0)
102
+ M = min(num_tokens, CHUNK_SIZE)
103
+
104
+ get_config_func = functools.partial(
105
+ try_get_optimal_moe_config,
106
+ layer.w13_weight.size(),
107
+ layer.w2_weight.size(),
108
+ top_k,
109
+ config_dtype,
110
+ block_shape=layer.quant_method.moe_quant_config.block_shape,
111
+ )
112
+
113
+ config = get_config_func(M)
114
+ (
115
+ sorted_token_ids_lora,
116
+ expert_ids_lora,
117
+ num_tokens_post_padded_lora,
118
+ ) = self.punica_wrapper.moe_lora_align_block_size(
119
+ curr_topk_ids,
120
+ num_tokens,
121
+ config["BLOCK_SIZE_M"],
122
+ global_num_experts,
123
+ max_loras,
124
+ expert_map,
125
+ )
126
+
127
+ moe_state_dict["sorted_token_ids_lora"] = sorted_token_ids_lora
128
+ moe_state_dict["expert_ids_lora"] = expert_ids_lora
129
+ moe_state_dict["num_tokens_post_padded_lora"] = (
130
+ num_tokens_post_padded_lora
131
+ )
132
+
133
+ w13_lora_a_stacked = [self.w1_lora_a_stacked, self.w3_lora_a_stacked]
134
+ w13_lora_b_stacked = [self.w1_lora_b_stacked, self.w3_lora_b_stacked]
135
+ max_lora_rank = self.w1_lora_a_stacked.shape[-2]
136
+ expert_ids_lora = expert_ids_lora.view(max_loras, -1)
137
+ sorted_token_ids_lora = sorted_token_ids_lora.view(max_loras, -1)
138
+
139
+ self.punica_wrapper.add_lora_fused_moe(
140
+ input.view(-1, top_k, input.shape[-1]),
141
+ hidden_states,
142
+ w13_lora_a_stacked,
143
+ w13_lora_b_stacked,
144
+ topk_weights,
145
+ sorted_token_ids_lora,
146
+ expert_ids_lora,
147
+ num_tokens_post_padded_lora,
148
+ max_lora_rank,
149
+ top_k,
150
+ config,
151
+ )
152
+
153
+ result = func(*args, **kwargs)
154
+
155
+ moe_state_dict["intermediate_cache2"] = output
156
+ return result
157
+
158
+ return wrapper
159
+
160
+ def moe_sum_decorator(layer, func):
161
+ def wrapper(*args, **kwargs):
162
+ hidden_states = moe_state_dict["hidden_states"]
163
+ topk_weights = moe_state_dict["topk_weights"]
164
+ max_loras = moe_state_dict["max_loras"]
165
+
166
+ config_dtype = _get_config_dtype_str(
167
+ dtype=hidden_states.dtype,
168
+ use_fp8_w8a8=False,
169
+ use_int8_w8a16=False,
170
+ use_int4_w4a16=False,
171
+ )
172
+ CHUNK_SIZE = envs.VLLM_FUSED_MOE_CHUNK_SIZE
173
+ num_tokens = hidden_states.size(0)
174
+ M = min(num_tokens, CHUNK_SIZE)
175
+
176
+ get_config_func = functools.partial(
177
+ try_get_optimal_moe_config,
178
+ layer.w13_weight.size(),
179
+ layer.w2_weight.size(),
180
+ top_k,
181
+ config_dtype,
182
+ block_shape=layer.quant_method.moe_quant_config.block_shape,
183
+ )
184
+
185
+ config = get_config_func(M)
186
+
187
+ sorted_token_ids_lora = moe_state_dict["sorted_token_ids_lora"]
188
+ expert_ids_lora = moe_state_dict["expert_ids_lora"]
189
+ num_tokens_post_padded_lora = moe_state_dict[
190
+ "num_tokens_post_padded_lora"
191
+ ]
192
+
193
+ expert_ids_lora = expert_ids_lora.view(max_loras, -1)
194
+ sorted_token_ids_lora = sorted_token_ids_lora.view(max_loras, -1)
195
+ intermediate_cache2 = moe_state_dict["intermediate_cache2"]
196
+ intermediate_cache3 = args[0]
197
+ max_lora_rank = self.w1_lora_a_stacked.shape[-2]
198
+ self.punica_wrapper.add_lora_fused_moe(
199
+ intermediate_cache3,
200
+ intermediate_cache2,
201
+ [self.w2_lora_a_stacked],
202
+ [self.w2_lora_b_stacked],
203
+ topk_weights,
204
+ sorted_token_ids_lora,
205
+ expert_ids_lora,
206
+ num_tokens_post_padded_lora,
207
+ max_lora_rank,
208
+ top_k,
209
+ config,
210
+ True,
211
+ )
212
+
213
+ result = func(*args, **kwargs)
214
+ return result
215
+
216
+ return wrapper
217
+
218
+ fused_experts = m_fused_moe_fn.fused_experts
219
+
220
+ m_fused_moe_fn.forward = fwd_decorator(self.base_layer, m_fused_moe_fn.forward)
221
+ fused_experts.activation = act_decorator(
222
+ self.base_layer, fused_experts.activation
223
+ )
224
+ fused_experts.moe_sum = moe_sum_decorator(
225
+ self.base_layer, fused_experts.moe_sum
226
+ )
227
+
228
+ self.base_layer.quant_method.old_fused_experts = (
229
+ self.base_layer.quant_method.fused_experts
230
+ )
231
+ self.base_layer.quant_method.fused_experts = m_fused_moe_fn
232
+
233
+ def create_lora_weights(
234
+ self,
235
+ max_loras: int,
236
+ lora_config: LoRAConfig,
237
+ model_config: PretrainedConfig | None = None,
238
+ ) -> None:
239
+ """Initializes lora matrices."""
240
+
241
+ assert not self.base_layer.use_ep, (
242
+ "EP support for Fused MoE LoRA is not implemented yet."
243
+ )
244
+
245
+ self.w1_lora_a_stacked = torch.zeros(
246
+ (
247
+ max_loras,
248
+ self.base_layer.global_num_experts,
249
+ lora_config.max_lora_rank,
250
+ self.base_layer.hidden_size,
251
+ ),
252
+ dtype=lora_config.lora_dtype,
253
+ device=self.device,
254
+ )
255
+ self.w1_lora_b_stacked = torch.zeros(
256
+ (
257
+ max_loras,
258
+ self.base_layer.global_num_experts,
259
+ self.base_layer.intermediate_size_per_partition,
260
+ lora_config.max_lora_rank,
261
+ ),
262
+ dtype=lora_config.lora_dtype,
263
+ device=self.device,
264
+ )
265
+
266
+ self.w2_lora_a_stacked = torch.zeros(
267
+ (
268
+ max_loras,
269
+ self.base_layer.global_num_experts,
270
+ lora_config.max_lora_rank,
271
+ self.base_layer.intermediate_size_per_partition,
272
+ ),
273
+ dtype=lora_config.lora_dtype,
274
+ device=self.device,
275
+ )
276
+ self.w2_lora_b_stacked = torch.zeros(
277
+ (
278
+ max_loras,
279
+ self.base_layer.global_num_experts,
280
+ self.base_layer.hidden_size,
281
+ lora_config.max_lora_rank,
282
+ ),
283
+ dtype=lora_config.lora_dtype,
284
+ device=self.device,
285
+ )
286
+
287
+ self.w3_lora_a_stacked = torch.zeros(
288
+ (
289
+ max_loras,
290
+ self.base_layer.global_num_experts,
291
+ lora_config.max_lora_rank,
292
+ self.base_layer.hidden_size,
293
+ ),
294
+ dtype=lora_config.lora_dtype,
295
+ device=self.device,
296
+ )
297
+ self.w3_lora_b_stacked = torch.zeros(
298
+ (
299
+ max_loras,
300
+ self.base_layer.global_num_experts,
301
+ self.base_layer.intermediate_size_per_partition,
302
+ lora_config.max_lora_rank,
303
+ ),
304
+ dtype=lora_config.lora_dtype,
305
+ device=self.device,
306
+ )
307
+
308
+ self.base_layer.w1_lora_a_stacked = self.w1_lora_a_stacked
309
+ self.base_layer.w1_lora_b_stacked = self.w1_lora_b_stacked
310
+ self.base_layer.w2_lora_a_stacked = self.w2_lora_a_stacked
311
+ self.base_layer.w2_lora_b_stacked = self.w2_lora_b_stacked
312
+ self.base_layer.w3_lora_a_stacked = self.w3_lora_a_stacked
313
+ self.base_layer.w3_lora_b_stacked = self.w3_lora_b_stacked
314
+ # They will be used by 'LoRALayerWeights.create_dummy_lora_weights'
315
+ # to create a dummy LoRA weights.
316
+ self.lora_a_stacked = []
317
+ self.lora_b_stacked = []
318
+ for lora_id in range(max_loras):
319
+ for experts_id in range(self.base_layer.global_num_experts):
320
+ # gate_proj,down_proj,up_proj
321
+ self.lora_a_stacked.append(self.w1_lora_a_stacked[lora_id][experts_id])
322
+ self.lora_a_stacked.append(self.w2_lora_a_stacked[lora_id][experts_id])
323
+ self.lora_a_stacked.append(self.w3_lora_a_stacked[lora_id][experts_id])
324
+
325
+ self.lora_b_stacked.append(self.w1_lora_b_stacked[lora_id][experts_id])
326
+ self.lora_b_stacked.append(self.w2_lora_b_stacked[lora_id][experts_id])
327
+ self.lora_b_stacked.append(self.w3_lora_b_stacked[lora_id][experts_id])
328
+
329
+ def reset_lora(self, index: int):
330
+ """Resets the lora weights at index back to 0."""
331
+ self.w1_lora_a_stacked[index] = 0
332
+ self.w1_lora_b_stacked[index] = 0
333
+ self.w3_lora_a_stacked[index] = 0
334
+ self.w3_lora_b_stacked[index] = 0
335
+ self.w2_lora_a_stacked[index] = 0
336
+ self.w2_lora_b_stacked[index] = 0
337
+
338
+ def set_lora(
339
+ self,
340
+ index: int,
341
+ lora_a: torch.Tensor,
342
+ lora_b: torch.Tensor,
343
+ embeddings_tensor: torch.Tensor | None,
344
+ bias: torch.Tensor | None = None,
345
+ ):
346
+ """Overwrites lora tensors at index."""
347
+ for eid in range(len(lora_a) // 3):
348
+ w1_lora_a = lora_a[eid * 3]
349
+ w2_lora_a = lora_a[eid * 3 + 1]
350
+ w3_lora_a = lora_a[eid * 3 + 2]
351
+ w1_lora_b = lora_b[eid * 3]
352
+ w2_lora_b = lora_b[eid * 3 + 1]
353
+ w3_lora_b = lora_b[eid * 3 + 2]
354
+
355
+ if self.tp_size > 1:
356
+ shard_size = self.base_layer.intermediate_size_per_partition
357
+ start_idx = self.tp_rank * shard_size
358
+ end_idx = (self.tp_rank + 1) * shard_size
359
+
360
+ w1_lora_b = w1_lora_b[start_idx:end_idx, :]
361
+ w3_lora_b = w3_lora_b[start_idx:end_idx, :]
362
+ w2_lora_a = w2_lora_a[:, start_idx:end_idx]
363
+
364
+ self.w1_lora_a_stacked[
365
+ index, eid, : w1_lora_a.shape[0], : w1_lora_a.shape[1]
366
+ ].copy_(w1_lora_a, non_blocking=True)
367
+
368
+ self.w3_lora_a_stacked[
369
+ index, eid, : w3_lora_a.shape[0], : w3_lora_a.shape[1]
370
+ ].copy_(w3_lora_a, non_blocking=True)
371
+
372
+ self.w2_lora_b_stacked[
373
+ index, eid, : w2_lora_b.shape[0], : w2_lora_b.shape[1]
374
+ ].copy_(w2_lora_b, non_blocking=True)
375
+
376
+ self.w1_lora_b_stacked[
377
+ index, eid, : w1_lora_b.shape[0], : w1_lora_b.shape[1]
378
+ ].copy_(w1_lora_b, non_blocking=True)
379
+ self.w3_lora_b_stacked[
380
+ index, eid, : w3_lora_b.shape[0], : w3_lora_b.shape[1]
381
+ ].copy_(w3_lora_b, non_blocking=True)
382
+ self.w2_lora_a_stacked[
383
+ index, eid, : w2_lora_a.shape[0], : w2_lora_a.shape[1]
384
+ ].copy_(w2_lora_a, non_blocking=True)
385
+
386
+ @classmethod
387
+ def can_replace_layer(
388
+ cls,
389
+ source_layer: nn.Module,
390
+ lora_config: LoRAConfig,
391
+ packed_modules_list: list,
392
+ model_config: PretrainedConfig | None,
393
+ ) -> bool:
394
+ """Returns True if the layer can be replaced by this LoRA layer."""
395
+ # return type(source_layer) is FusedMoE
396
+ return isinstance(source_layer, FusedMoE)
397
+
398
+ def forward(self, *args, **kwargs):
399
+ return self.base_layer.forward(*args, **kwargs)
400
+
401
+ def maybe_all_reduce_tensor_model_parallel(self, *args, **kwargs):
402
+ return self.base_layer.maybe_all_reduce_tensor_model_parallel(*args, **kwargs)
403
+
404
+ @property
405
+ def _shared_experts(self):
406
+ return self.base_layer._shared_experts
407
+
408
+ @property
409
+ def quant_method(self):
410
+ return self.base_layer.quant_method