-
-
Notifications
You must be signed in to change notification settings - Fork 19.2k
[LoRA] MoE LoRA Refactor #40338
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[LoRA] MoE LoRA Refactor #40338
Changes from 2 commits
85cf592
9c64777
7b81c9e
80d0188
9350a67
b3d1ea6
13caeb4
1455838
51be12a
c2dbb14
4f3b7f9
550e19d
019cfa1
d1ae808
61d7746
ea4a8fd
5600221
cd29a49
7707bf3
166386e
6fe6601
99c00a2
7c855e4
ce0f6c3
5c1fe18
3efd6c5
4583130
b632e53
987d67a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,6 +1,5 @@ | ||||||
| # SPDX-License-Identifier: Apache-2.0 | ||||||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||||||
| import functools | ||||||
|
|
||||||
| import torch | ||||||
| import torch.nn as nn | ||||||
|
|
@@ -14,31 +13,20 @@ | |||||
| ) | ||||||
| from vllm.distributed.utils import divide | ||||||
| from vllm.lora.layers.base import BaseLayerWithLoRA | ||||||
| from vllm.lora.ops.triton_ops.utils import get_lora_op_configs | ||||||
| from vllm.lora.lora_context import MoELoRAContext | ||||||
| from vllm.model_executor.layers.fused_moe import FusedMoE | ||||||
| from vllm.model_executor.layers.fused_moe.config import ( | ||||||
| _get_config_dtype_str, | ||||||
| ) | ||||||
| from vllm.model_executor.layers.fused_moe.fused_marlin_moe import ( | ||||||
| MarlinExperts, | ||||||
| ) | ||||||
| from vllm.model_executor.layers.fused_moe.fused_moe import ( | ||||||
| TritonExperts, | ||||||
| ) | ||||||
| from vllm.model_executor.layers.fused_moe.fused_moe_modular_method import ( | ||||||
| FusedMoEModularMethod, | ||||||
| ) | ||||||
| from vllm.model_executor.layers.fused_moe.gpt_oss_triton_kernels_moe import ( | ||||||
| UnfusedOAITritonExperts, | ||||||
| ) | ||||||
| from vllm.model_executor.layers.fused_moe.modular_kernel import ( | ||||||
| FusedMoEExpertsModular, | ||||||
| FusedMoEKernel, | ||||||
| ) | ||||||
| from vllm.model_executor.layers.fused_moe.prepare_finalize import ( | ||||||
| MoEPrepareAndFinalizeNoDPEPModular, | ||||||
| ) | ||||||
|
|
||||||
| from .utils import _get_lora_device, try_get_optimal_moe_lora_config | ||||||
| from .utils import _get_lora_device | ||||||
|
|
||||||
|
|
||||||
| class FusedMoEWithLoRA(BaseLayerWithLoRA): | ||||||
|
|
@@ -58,299 +46,51 @@ def __init__(self, base_layer: FusedMoE) -> None: | |||||
| # For non-gated MoE (is_act_and_mul=False), only 1 slice is needed | ||||||
| # since there's only up_proj (w1), not gate_proj + up_proj (w1 + w3) | ||||||
| self._w13_slices = 2 if base_layer.moe_config.is_act_and_mul else 1 | ||||||
| self._inject_lora_into_fused_moe() | ||||||
|
|
||||||
| def _normalize_keys(self, config: dict[str, int | None]) -> dict[str, int | None]: | ||||||
| normalized_config = {} | ||||||
| for key, value in config.items(): | ||||||
| if key.islower(): | ||||||
| if key.startswith("block_"): | ||||||
| normalized_key = "BLOCK_SIZE_" + key.split("_")[-1].upper() | ||||||
| else: | ||||||
| normalized_key = key.upper() | ||||||
| else: | ||||||
| normalized_key = key | ||||||
| normalized_config[normalized_key] = value | ||||||
| return normalized_config | ||||||
|
|
||||||
| def _get_lora_moe_configs( | ||||||
| self, | ||||||
| op_prefix: str, | ||||||
| num_loras: int, | ||||||
| rank: int, | ||||||
| num_slices: int, | ||||||
| M: int, | ||||||
| layer: FusedMoE, | ||||||
| top_k: int, | ||||||
| config_dtype: str, | ||||||
| ): | ||||||
| if envs.VLLM_TUNED_CONFIG_FOLDER: | ||||||
| hidden_size = layer.hidden_size | ||||||
| intermediate_size = ( | ||||||
| self.w2_lora_a_stacked[0].shape[-1] | ||||||
| if op_prefix == "w2" | ||||||
| else self.w13_lora_b_stacked[0].shape[-2] | ||||||
| ) | ||||||
| shrink_config = get_lora_op_configs( | ||||||
| op_type=f"fused_moe_lora_{op_prefix}_shrink", | ||||||
| max_loras=num_loras, | ||||||
| batch=M, | ||||||
| hidden_size=hidden_size, | ||||||
| rank=rank, | ||||||
| num_slices=num_slices, | ||||||
| moe_intermediate_size=intermediate_size, | ||||||
| ) | ||||||
| expand_config = get_lora_op_configs( | ||||||
| op_type=f"fused_moe_lora_{op_prefix}_expand", | ||||||
| max_loras=num_loras, | ||||||
| batch=M, | ||||||
| hidden_size=hidden_size, # lora_a_stacked.shape[-1], | ||||||
| rank=rank, | ||||||
| num_slices=num_slices, | ||||||
| moe_intermediate_size=intermediate_size, # lora_b_stacked.shape[-2], | ||||||
| ) | ||||||
| else: # fall back to the default config | ||||||
| get_config_func = functools.partial( | ||||||
| try_get_optimal_moe_lora_config, | ||||||
| w1_shape=layer.w13_weight.shape, | ||||||
| w2_shape=layer.w2_weight.shape, | ||||||
| rank=rank, | ||||||
| top_k=top_k, | ||||||
| dtype=config_dtype, | ||||||
| M=M, | ||||||
| block_shape=layer.quant_method.moe_quant_config.block_shape, | ||||||
| ) | ||||||
| shrink_config = get_config_func( | ||||||
| op_type=f"fused_moe_lora_{op_prefix}_shrink" | ||||||
| ) | ||||||
| expand_config = get_config_func( | ||||||
| op_type=f"fused_moe_lora_{op_prefix}_expand" | ||||||
| ) | ||||||
| shrink_config = self._normalize_keys(shrink_config) | ||||||
| expand_config = self._normalize_keys(expand_config) | ||||||
| return shrink_config, expand_config | ||||||
|
|
||||||
| def _inject_lora_into_fused_moe(self): | ||||||
| moe_state_dict = {} | ||||||
| top_k = self.base_layer.top_k | ||||||
|
|
||||||
| self.base_layer.ensure_moe_quant_config_init() | ||||||
| quant_config = self.base_layer.quant_method.moe_quant_config | ||||||
|
|
||||||
| if getattr(self.base_layer.quant_method, "supports_internal_mk", False): | ||||||
| # Use the existing modular kernel from the quant method | ||||||
| m_fused_moe_fn = self.base_layer.quant_method.moe_kernel | ||||||
| moe_kernel = self.base_layer.quant_method.moe_kernel | ||||||
| # Don't let the kernel own shared experts so the runner can | ||||||
| # overlap them with routed experts via a separate CUDA stream. | ||||||
| m_fused_moe_fn.shared_experts = None | ||||||
| moe_kernel.shared_experts = None | ||||||
| else: | ||||||
| # Create a new modular kernel via select_gemm_impl. | ||||||
| # Don't pass shared_experts to the kernel so the runner can | ||||||
| # overlap them with routed experts via a separate CUDA stream. | ||||||
| prepare_finalize = MoEPrepareAndFinalizeNoDPEPModular() | ||||||
| m_fused_moe_fn = FusedMoEKernel( | ||||||
| moe_kernel = FusedMoEKernel( | ||||||
| prepare_finalize, | ||||||
| self.base_layer.quant_method.select_gemm_impl( | ||||||
| prepare_finalize, self.base_layer | ||||||
| ), | ||||||
| ) | ||||||
|
|
||||||
| if quant_config.use_mxfp4_w4a16: | ||||||
| assert isinstance( | ||||||
| m_fused_moe_fn.impl.fused_experts, | ||||||
| (MarlinExperts, UnfusedOAITritonExperts), | ||||||
| ) | ||||||
| else: | ||||||
| assert isinstance(m_fused_moe_fn.impl.fused_experts, TritonExperts) | ||||||
|
|
||||||
| def fwd_decorator(layer, func): | ||||||
| def wrapper(*args, **kwargs): | ||||||
| moe_state_dict["hidden_states"] = kwargs["hidden_states"] | ||||||
| moe_state_dict["topk_ids"] = kwargs["topk_ids"] | ||||||
| moe_state_dict["topk_weights"] = kwargs["topk_weights"] | ||||||
| moe_state_dict["expert_map"] = kwargs["expert_map"] | ||||||
| moe_state_dict["apply_router_weight_on_input"] = kwargs[ | ||||||
| "apply_router_weight_on_input" | ||||||
| ] | ||||||
| result = func(*args, **kwargs) | ||||||
| return result | ||||||
|
|
||||||
| return wrapper | ||||||
|
|
||||||
| def act_decorator(layer, func): | ||||||
| def wrapper(*args, **kwargs): | ||||||
| _, output, input = args | ||||||
|
|
||||||
| hidden_states = moe_state_dict["hidden_states"] | ||||||
| topk_weights = moe_state_dict["topk_weights"] | ||||||
| curr_topk_ids = moe_state_dict["topk_ids"] | ||||||
|
|
||||||
| expert_map = moe_state_dict["expert_map"] | ||||||
|
|
||||||
| config_dtype = _get_config_dtype_str( | ||||||
| dtype=hidden_states.dtype, | ||||||
| use_fp8_w8a8=False, | ||||||
| use_int8_w8a16=False, | ||||||
| use_int4_w4a16=False, | ||||||
| ) | ||||||
| num_tokens = hidden_states.size(0) | ||||||
| M = num_tokens | ||||||
| max_lora_rank = self.w13_lora_a_stacked[0].shape[-2] | ||||||
| shrink_config, expand_config = self._get_lora_moe_configs( | ||||||
| op_prefix="w13", | ||||||
| num_loras=self.max_loras, | ||||||
| rank=max_lora_rank, | ||||||
| num_slices=self._w13_slices, | ||||||
| M=M, | ||||||
| layer=layer, | ||||||
| top_k=top_k, | ||||||
| config_dtype=config_dtype, | ||||||
| ) | ||||||
|
|
||||||
| # SPARSITY_FACTOR is a heuristic margin ensuring tokens * top_k | ||||||
| # activates only a small fraction of total experts * loras. | ||||||
| SPARSITY_FACTOR = 8 | ||||||
| naive_block_assignment = ( | ||||||
| expert_map is None | ||||||
| and num_tokens * top_k * SPARSITY_FACTOR | ||||||
| <= self.base_layer.local_num_experts * self.max_loras | ||||||
| ) | ||||||
|
|
||||||
| # get the block size of m from customized config or default config | ||||||
| ( | ||||||
| token_lora_mapping, | ||||||
| sorted_token_ids_lora, | ||||||
| expert_ids_lora, | ||||||
| num_tokens_post_padded_lora, | ||||||
| ) = self.punica_wrapper.moe_lora_align_block_size( | ||||||
| curr_topk_ids, | ||||||
| num_tokens, | ||||||
| shrink_config["BLOCK_SIZE_M"], | ||||||
| self.base_layer.local_num_experts, | ||||||
| self.max_loras, | ||||||
| self.adapter_enabled, | ||||||
| expert_map, | ||||||
| naive_block_assignment=naive_block_assignment, | ||||||
| ) | ||||||
|
|
||||||
| moe_state_dict["sorted_token_ids_lora"] = sorted_token_ids_lora | ||||||
| moe_state_dict["expert_ids_lora"] = expert_ids_lora | ||||||
| moe_state_dict["num_tokens_post_padded_lora"] = ( | ||||||
| num_tokens_post_padded_lora | ||||||
| ) | ||||||
| moe_state_dict["token_lora_mapping"] = token_lora_mapping | ||||||
|
|
||||||
| if sorted_token_ids_lora is not None: | ||||||
| expert_ids_lora = expert_ids_lora.view(self.max_loras, -1) | ||||||
| sorted_token_ids_lora = sorted_token_ids_lora.view( | ||||||
| self.max_loras, -1 | ||||||
| ) | ||||||
| # | ||||||
|
|
||||||
| self.punica_wrapper.add_lora_fused_moe( | ||||||
| input.view(-1, top_k, input.shape[-1]), | ||||||
| hidden_states, | ||||||
| self.w13_lora_a_stacked, | ||||||
| self.w13_lora_b_stacked, | ||||||
| topk_weights, | ||||||
| sorted_token_ids_lora, | ||||||
| expert_ids_lora, | ||||||
| num_tokens_post_padded_lora, | ||||||
| max_lora_rank, | ||||||
| top_k, | ||||||
| shrink_config, ## pass the shrink config | ||||||
| expand_config, ## pass the expand config | ||||||
| self.adapter_enabled, | ||||||
| fully_sharded=self.fully_sharded, | ||||||
| token_lora_mapping=token_lora_mapping, | ||||||
| ) | ||||||
|
|
||||||
| result = func(*args, **kwargs) | ||||||
|
|
||||||
| moe_state_dict["intermediate_cache2"] = output | ||||||
| return result | ||||||
|
|
||||||
| return wrapper | ||||||
|
|
||||||
| def moe_sum_decorator(layer, func): | ||||||
| def wrapper(*args, **kwargs): | ||||||
| hidden_states = moe_state_dict["hidden_states"] | ||||||
| topk_weights = moe_state_dict["topk_weights"] | ||||||
|
|
||||||
| config_dtype = _get_config_dtype_str( | ||||||
| dtype=hidden_states.dtype, | ||||||
| use_fp8_w8a8=False, | ||||||
| use_int8_w8a16=False, | ||||||
| use_int4_w4a16=False, | ||||||
| ) | ||||||
| num_tokens = hidden_states.size(0) | ||||||
| M = num_tokens | ||||||
| max_lora_rank = self.w2_lora_a_stacked[0].shape[-2] | ||||||
| shrink_config, expand_config = self._get_lora_moe_configs( | ||||||
| op_prefix="w2", | ||||||
| num_loras=self.max_loras, | ||||||
| rank=max_lora_rank, | ||||||
| num_slices=1, | ||||||
| M=M, | ||||||
| layer=layer, | ||||||
| top_k=top_k, | ||||||
| config_dtype=config_dtype, | ||||||
| ) | ||||||
|
|
||||||
| sorted_token_ids_lora = moe_state_dict["sorted_token_ids_lora"] | ||||||
| expert_ids_lora = moe_state_dict["expert_ids_lora"] | ||||||
| num_tokens_post_padded_lora = moe_state_dict[ | ||||||
| "num_tokens_post_padded_lora" | ||||||
| ] | ||||||
| token_lora_mapping = moe_state_dict.get("token_lora_mapping") | ||||||
|
|
||||||
| if sorted_token_ids_lora is not None: | ||||||
| expert_ids_lora = expert_ids_lora.view(self.max_loras, -1) | ||||||
| sorted_token_ids_lora = sorted_token_ids_lora.view( | ||||||
| self.max_loras, -1 | ||||||
| ) | ||||||
| intermediate_cache2 = moe_state_dict["intermediate_cache2"] | ||||||
| intermediate_cache3 = args[0] | ||||||
|
|
||||||
| shard_size_w2 = divide(self.base_layer.hidden_size, self.tp_size) | ||||||
|
|
||||||
| self.punica_wrapper.add_lora_fused_moe( | ||||||
| intermediate_cache3, | ||||||
| intermediate_cache2, | ||||||
| self.w2_lora_a_stacked, | ||||||
| self.w2_lora_b_stacked, | ||||||
| topk_weights, | ||||||
| sorted_token_ids_lora, | ||||||
| expert_ids_lora, | ||||||
| num_tokens_post_padded_lora, | ||||||
| max_lora_rank, | ||||||
| top_k, | ||||||
| shrink_config, ## pass the shrink config | ||||||
| expand_config, ## pass the expand config | ||||||
| self.adapter_enabled, | ||||||
| True, | ||||||
| fully_sharded=self.fully_sharded, | ||||||
| offset=shard_size_w2 * self.tp_rank if self.fully_sharded else 0, | ||||||
| token_lora_mapping=token_lora_mapping, | ||||||
| ) | ||||||
|
|
||||||
| result = func(*args, **kwargs) | ||||||
| return result | ||||||
|
|
||||||
| return wrapper | ||||||
|
|
||||||
| fused_experts = m_fused_moe_fn.impl.fused_experts | ||||||
|
|
||||||
| m_fused_moe_fn.apply = fwd_decorator(self.base_layer, m_fused_moe_fn.apply) | ||||||
| fused_experts.activation = act_decorator( | ||||||
| self.base_layer, fused_experts.activation | ||||||
| ) | ||||||
| fused_experts.moe_sum = moe_sum_decorator( | ||||||
| self.base_layer, fused_experts.moe_sum | ||||||
| assert ( | ||||||
| isinstance(moe_kernel.fused_experts, FusedMoEExpertsModular) | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The check
Suggested change
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could use the You might also need to add a |
||||||
| and moe_kernel.fused_experts.supports_lora() | ||||||
| ), ( | ||||||
| f"{type(moe_kernel.fused_experts).__name__} does not support LoRA. " | ||||||
| "For unquantized MoE, set moe_backend='triton' or moe_backend='auto' " | ||||||
| "(auto selects Triton automatically when LoRA is enabled). " | ||||||
| "For quantized MoE, implement supports_lora() -> True and handle " | ||||||
| "lora_context in apply()." | ||||||
| ) | ||||||
| # TODO(bnell): find a less intrusive way to handle this. | ||||||
| self.base_layer._replace_quant_method( | ||||||
| FusedMoEModularMethod(self.base_layer.quant_method, m_fused_moe_fn) | ||||||
| FusedMoEModularMethod(self.base_layer.quant_method, moe_kernel) | ||||||
| ) | ||||||
|
|
||||||
| def _build_lora_context(self): | ||||||
| return MoELoRAContext( | ||||||
| w13_lora_a_stacked=self.w13_lora_a_stacked, | ||||||
| w13_lora_b_stacked=self.w13_lora_b_stacked, | ||||||
| w2_lora_a_stacked=self.w2_lora_a_stacked, | ||||||
| w2_lora_b_stacked=self.w2_lora_b_stacked, | ||||||
| adapter_enabled=self.adapter_enabled, | ||||||
| max_loras=self.max_loras, | ||||||
| top_k=self.base_layer.top_k, | ||||||
| w13_num_slices=self._w13_slices, | ||||||
| fully_sharded=self.fully_sharded, | ||||||
| tp_rank=self.tp_rank, | ||||||
| tp_size=self.tp_size, | ||||||
| local_num_experts=self.base_layer.local_num_experts, | ||||||
| punica_wrapper=self.punica_wrapper, | ||||||
| use_tuned_config=bool(envs.VLLM_TUNED_CONFIG_FOLDER), | ||||||
| ) | ||||||
|
|
||||||
| def _create_lora_a_weights( | ||||||
|
|
@@ -589,6 +329,10 @@ def set_lora( | |||||
| index, :, : sliced_w2_lora_b.shape[1], : sliced_w2_lora_b.shape[2] | ||||||
| ].copy_(sliced_w2_lora_b, non_blocking=True) | ||||||
|
|
||||||
| def set_mapping(self, punica_wrapper): | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this happen at runtime or is this part of the LoRA setup?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is LoRA setup, not runtime. MoELoRAContext captures references to it so the experts kernel sees fresh values without rebinding |
||||||
| super().set_mapping(punica_wrapper) | ||||||
| self.base_layer._lora_context = self._build_lora_context() | ||||||
|
|
||||||
| def forward(self, *args, **kwargs): | ||||||
| return self.base_layer.forward(*args, **kwargs) | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we know if this case is ever hit now? Most methods have been switched over to the new MK initialization pattern (
_setup_kernel)