-
Notifications
You must be signed in to change notification settings - Fork 7.6k
[3/n] lora moe - Support Qwen3-VL-30B-A3B-Instruct #21469
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
Changes from 5 commits
9f602f9
4f30d8c
add28dc
767655c
3ea7296
4fd3f01
bbbf640
6f29b1a
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -711,6 +711,9 @@ def __init__( | |||||
| # initializes FusedMoE with its own moe_runner for base path | ||||||
| super().__init__(base_layer, lora_backend) | ||||||
|
|
||||||
| self.experts_shared_outer_loras: bool = False | ||||||
| self.quant_method = base_layer.quant_method | ||||||
|
|
||||||
| self.tp_size = getattr(base_layer, "moe_tp_size", 1) | ||||||
| self.tp_rank = getattr(base_layer, "moe_tp_rank", 0) | ||||||
| self.intermediate_size_per_partition = getattr( | ||||||
|
|
@@ -782,6 +785,7 @@ def _get_lora_info(self): | |||||
| adapter_enabled=adapter_enabled, | ||||||
| max_lora_rank=max_lora_rank, | ||||||
| num_experts=self.base_layer.num_experts, | ||||||
| experts_shared_outer_loras=self.experts_shared_outer_loras, | ||||||
| tp_size=self.tp_size, | ||||||
| tp_rank=self.tp_rank, | ||||||
| hidden_size=getattr(self.base_layer, "hidden_size", 0), | ||||||
|
|
@@ -838,35 +842,75 @@ def slice_lora_a_weights(self, A: torch.Tensor, tp_rank: int): | |||||
| def slice_lora_b_weights(self, B: torch.Tensor, tp_rank: int): | ||||||
| return B | ||||||
|
|
||||||
| def slice_moe_lora_a_weights( | ||||||
| self, A: torch.Tensor, tp_rank: int, target_module: str | ||||||
| ) -> torch.Tensor: | ||||||
| def slice_moe_lora_a_weights(self, A, tp_rank: int, target_module: str): | ||||||
| """Slice LoRA A weights for MoE with TP. | ||||||
|
|
||||||
| Accepts: | ||||||
| - 2D tensor [rank, hidden] (single expert) | ||||||
| - 3D tensor [num_experts_or_1, rank, hidden] | ||||||
| - dict {expert_id: 2D tensor} | ||||||
|
|
||||||
| Per-expert weight shapes: | ||||||
| gate_up_proj_moe A: [rank, hidden_size] — input is full hidden_states, no slice | ||||||
| down_proj_moe A: [rank, intermediate_size] — input is sharded intermediate | ||||||
| """ | ||||||
| if self.tp_size <= 1: | ||||||
| return A | ||||||
| if isinstance(A, dict): | ||||||
| return { | ||||||
| eid: self._slice_moe_a_2d(w, tp_rank, target_module) | ||||||
| for eid, w in A.items() | ||||||
| } | ||||||
| if isinstance(A, torch.Tensor) and A.dim() == 3: | ||||||
| return torch.stack( | ||||||
| [ | ||||||
| self._slice_moe_a_2d(A[i], tp_rank, target_module) | ||||||
| for i in range(A.shape[0]) | ||||||
| ] | ||||||
| ) | ||||||
| return self._slice_moe_a_2d(A, tp_rank, target_module) | ||||||
|
|
||||||
| def _slice_moe_a_2d( | ||||||
| self, A: torch.Tensor, tp_rank: int, target_module: str | ||||||
| ) -> torch.Tensor: | ||||||
| if target_module == "down_proj_moe": | ||||||
| shard_size = self.intermediate_size_per_partition | ||||||
| start = tp_rank * shard_size | ||||||
| end = start + shard_size | ||||||
| return A[:, start:end].contiguous() | ||||||
| return A | ||||||
|
|
||||||
| def slice_moe_lora_b_weights( | ||||||
| self, B: torch.Tensor, tp_rank: int, target_module: str | ||||||
| ) -> torch.Tensor: | ||||||
| def slice_moe_lora_b_weights(self, B, tp_rank: int, target_module: str): | ||||||
|
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. Similar to
Suggested change
|
||||||
| """Slice LoRA B weights for MoE with TP. | ||||||
|
|
||||||
| Accepts: | ||||||
| - 2D tensor [output_dim, rank] (single expert) | ||||||
| - 3D tensor [num_experts_or_1, output_dim, rank] | ||||||
| - dict {expert_id: 2D tensor} | ||||||
|
|
||||||
| Per-expert weight shapes: | ||||||
| gate_up_proj_moe B: [intermediate_size*2, rank] — output matches sharded base w13 | ||||||
| down_proj_moe B: [hidden_size, rank] — output is all-reduced, no slice | ||||||
| """ | ||||||
| if self.tp_size <= 1: | ||||||
| return B | ||||||
| if isinstance(B, dict): | ||||||
| return { | ||||||
| eid: self._slice_moe_b_2d(w, tp_rank, target_module) | ||||||
| for eid, w in B.items() | ||||||
| } | ||||||
| if isinstance(B, torch.Tensor) and B.dim() == 3: | ||||||
| return torch.stack( | ||||||
| [ | ||||||
| self._slice_moe_b_2d(B[i], tp_rank, target_module) | ||||||
| for i in range(B.shape[0]) | ||||||
| ] | ||||||
| ) | ||||||
| return self._slice_moe_b_2d(B, tp_rank, target_module) | ||||||
|
|
||||||
| def _slice_moe_b_2d( | ||||||
| self, B: torch.Tensor, tp_rank: int, target_module: str | ||||||
| ) -> torch.Tensor: | ||||||
| if target_module == "gate_up_proj_moe": | ||||||
| shard_size = self.intermediate_size_per_partition | ||||||
| start = tp_rank * shard_size | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -36,6 +36,7 @@ | |||||||||||||||||||
| from sglang.srt.lora.mem_pool import LoRAMemoryPool | ||||||||||||||||||||
| from sglang.srt.lora.utils import ( | ||||||||||||||||||||
| LoRAType, | ||||||||||||||||||||
| auto_detect_lora_target_modules, | ||||||||||||||||||||
| get_normalized_target_modules, | ||||||||||||||||||||
| get_target_module_name, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
@@ -77,8 +78,10 @@ def __init__( | |||||||||||||||||||
| server_args.enable_lora_overlap_loading | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Store eviction policy from server args | ||||||||||||||||||||
| self.eviction_policy = server_args.lora_eviction_policy | ||||||||||||||||||||
| self._experts_shared_outer_override: Optional[bool] = ( | ||||||||||||||||||||
| server_args.experts_shared_outer_loras | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # LoRA backend for running sgemm kernels | ||||||||||||||||||||
| logger.info(f"Using {lora_backend} as backend of LoRA kernels.") | ||||||||||||||||||||
|
|
@@ -302,23 +305,33 @@ def update_lora_info(self): | |||||||||||||||||||
| if isinstance(module, FusedMoEWithLoRA) and all( | ||||||||||||||||||||
| x in self.target_modules for x in ["gate_up_proj", "down_proj"] | ||||||||||||||||||||
| ): | ||||||||||||||||||||
| gate_up_key = ( | ||||||||||||||||||||
| "gate_up_proj_moe" | ||||||||||||||||||||
| if "gate_up_proj_moe" in self.memory_pool.A_buffer | ||||||||||||||||||||
| else "gate_up_proj" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| down_key = ( | ||||||||||||||||||||
| "down_proj_moe" | ||||||||||||||||||||
| if "down_proj_moe" in self.memory_pool.A_buffer | ||||||||||||||||||||
| else "down_proj" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| gate_up_a = self.memory_pool.get_tensor( | ||||||||||||||||||||
| target_module="gate_up_proj_moe", | ||||||||||||||||||||
| target_module=gate_up_key, | ||||||||||||||||||||
| layer_id=layer_id, | ||||||||||||||||||||
| lora_type=LoRAType.LORA_A, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| gate_up_b = self.memory_pool.get_tensor( | ||||||||||||||||||||
| target_module="gate_up_proj_moe", | ||||||||||||||||||||
| target_module=gate_up_key, | ||||||||||||||||||||
| layer_id=layer_id, | ||||||||||||||||||||
| lora_type=LoRAType.LORA_B, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| down_a = self.memory_pool.get_tensor( | ||||||||||||||||||||
| target_module="down_proj_moe", | ||||||||||||||||||||
| target_module=down_key, | ||||||||||||||||||||
| layer_id=layer_id, | ||||||||||||||||||||
| lora_type=LoRAType.LORA_A, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| down_b = self.memory_pool.get_tensor( | ||||||||||||||||||||
| target_module="down_proj_moe", | ||||||||||||||||||||
| target_module=down_key, | ||||||||||||||||||||
| layer_id=layer_id, | ||||||||||||||||||||
| lora_type=LoRAType.LORA_B, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
@@ -386,6 +399,16 @@ def init_state( | |||||||||||||||||||
| target_modules=target_modules, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if self._experts_shared_outer_override is not None: | ||||||||||||||||||||
| self.experts_shared_outer_loras = self._experts_shared_outer_override | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| self.experts_shared_outer_loras = self._detect_shared_outer_loras() | ||||||||||||||||||||
| if self.experts_shared_outer_loras: | ||||||||||||||||||||
| logger.info( | ||||||||||||||||||||
| "Shared outer LoRA mode enabled: gate_up lora_A and " | ||||||||||||||||||||
| "down lora_B will be shared across experts (expert_dim=1)." | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| self.init_lora_modules() | ||||||||||||||||||||
| self.init_memory_pool() | ||||||||||||||||||||
| self.update_lora_info() | ||||||||||||||||||||
|
|
@@ -411,6 +434,26 @@ def init_lora_adapters(self, lora_paths: Optional[List[LoRARef]] = None): | |||||||||||||||||||
| f"Failed to load LoRA adapter {lora_ref.lora_name}: {result.error_message}" | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def _detect_shared_outer_loras(self) -> bool: | ||||||||||||||||||||
| """Auto-detect shared outer LoRA format from loaded adapter weights. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| MoE adapters with shared outer experts store 3D tensors where | ||||||||||||||||||||
| dim[0]=1 indicates weights shared across all experts, while | ||||||||||||||||||||
| dim[0]=num_experts indicates per-expert weights. | ||||||||||||||||||||
| Returns True if gate_up lora_A has expert_dim=1 (shared). | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| for adapter in self.loras.values(): | ||||||||||||||||||||
| for layer in adapter.layers: | ||||||||||||||||||||
| for name, weight in layer.weights.items(): | ||||||||||||||||||||
| if ( | ||||||||||||||||||||
| "gate_up_proj" in name | ||||||||||||||||||||
| and "lora_A" in name | ||||||||||||||||||||
| and weight.dim() == 3 | ||||||||||||||||||||
| ): | ||||||||||||||||||||
| return weight.shape[0] == 1 | ||||||||||||||||||||
| break | ||||||||||||||||||||
| return False | ||||||||||||||||||||
|
Comment on lines
+445
to
+455
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| def init_lora_shapes( | ||||||||||||||||||||
| self, | ||||||||||||||||||||
| max_lora_rank: Optional[int] = None, | ||||||||||||||||||||
|
|
@@ -424,24 +467,27 @@ def init_lora_shapes( | |||||||||||||||||||
|
|
||||||||||||||||||||
| for lora_id, config in self.configs.items(): | ||||||||||||||||||||
| # Handle PEFT shorthand strings like "all-linear" or "all". | ||||||||||||||||||||
| # These cannot be resolved to concrete module names without | ||||||||||||||||||||
| # inspecting the base model, so we require the user to specify | ||||||||||||||||||||
| # --lora-target-modules explicitly when such shorthands are used. | ||||||||||||||||||||
| if isinstance(config.target_modules, str): | ||||||||||||||||||||
| if config.target_modules in ("all-linear", "all"): | ||||||||||||||||||||
| if target_modules is not None: | ||||||||||||||||||||
| # CLI --lora-target-modules already provided; skip | ||||||||||||||||||||
| # per-adapter inference for this adapter. | ||||||||||||||||||||
| continue | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| lora_name = self.lora_refs[lora_id].lora_name | ||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||
| f"LoRA adapter '{lora_name}' uses " | ||||||||||||||||||||
| f"target_modules='{config.target_modules}' which cannot " | ||||||||||||||||||||
| "be resolved automatically. Please explicitly specify " | ||||||||||||||||||||
| "--lora-target-modules during server startup. You can " | ||||||||||||||||||||
| "specify 'all' to enable all supported module types." | ||||||||||||||||||||
| # Resolve by scanning the base model for all | ||||||||||||||||||||
| # LoRA-compatible linear modules. | ||||||||||||||||||||
| adapter_target_modules = auto_detect_lora_target_modules( | ||||||||||||||||||||
| self.base_model | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| logger.info( | ||||||||||||||||||||
| "LoRA adapter '%s' uses target_modules='%s'. " | ||||||||||||||||||||
| "Resolved to %s by inspecting the base model.", | ||||||||||||||||||||
| self.lora_refs[lora_id].lora_name, | ||||||||||||||||||||
| config.target_modules, | ||||||||||||||||||||
| sorted(adapter_target_modules), | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| self.target_modules.update(adapter_target_modules) | ||||||||||||||||||||
| continue | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| raise ValueError( | ||||||||||||||||||||
| f"SGLang does not recognize target_modules=" | ||||||||||||||||||||
|
|
@@ -585,6 +631,7 @@ def init_memory_pool(self): | |||||||||||||||||||
| base_model=self.base_model, | ||||||||||||||||||||
| eviction_policy=self.eviction_policy, | ||||||||||||||||||||
| lora_added_tokens_size=self.lora_added_tokens_size, | ||||||||||||||||||||
| experts_shared_outer_loras=self.experts_shared_outer_loras, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Initializing memory pool with base model | ||||||||||||||||||||
|
|
@@ -672,16 +719,17 @@ def init_lora_modules(self): | |||||||||||||||||||
| # The module should be converted if it is included in target_names | ||||||||||||||||||||
| if module_name.split(".")[-1] in self.target_modules: | ||||||||||||||||||||
| layer_id = get_layer_id(module_name) | ||||||||||||||||||||
| if layer_id is None: | ||||||||||||||||||||
|
||||||||||||||||||||
| if layer_id is None: | |
| if layer_id is None: | |
| logger.warning( | |
| "LoRA target module '%s' matched by name but no layer_id could be " | |
| "determined from module_name '%s'. Skipping LoRA wrapping for this " | |
| "module. This may indicate a naming or compatibility issue.", | |
| module_name.split(".")[-1], | |
| module_name, | |
| ) |
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.
For better code clarity and maintainability, consider adding type hints to this function signature. Based on the implementation, the type for
Aand the return type could beUnion[torch.Tensor, Dict[int, torch.Tensor]].You'll need to update the import at the top of the file: