Skip to content

Commit fa63e99

Browse files
Glitchfixlfengad
andauthored
perf(moe): speed up grouped MoE routing (NVIDIA#20)
## Summary This PR tightens the routing/unrouting path in `Qwen3VLMoeTextExpertsGroupedMm`: - keep routed token indices as a 1D vector instead of expanding them across the hidden dimension - use `index_select` / `index_add_` for route and combine instead of hidden-size-expanded `gather` / `scatter_add_` - update the standalone MoE comparison harness so it calls the current expert API and checks grouped output against the PyTorch reference The old path was doing a lot of unnecessary index traffic. That is mostly invisible for smaller routing configs, but it becomes very noticeable when `top_k` and the expert count go up. ## Benchmarks Environment: RTX 6000 Ada, `torch==2.10.0+cu128`, BF16 unless noted. Focused MoE harness: ```text python -m cosmos_framework.model.vfm.vlm.qwen3_vl_moe.moe_test naive: 13.81 ms optimized: 3.92 ms speedup: 3.53x relative diff: 0.00531 ``` Synthetic decoder stack throughput, including attention + norms + MoE, old grouped routing vs this PR: ```text E128_K8_I768, 8 layers, seq=1024 old: 134.58 ms, 7,608.7 tok/s new: 42.16 ms, 24,289.3 tok/s speedup: 3.19x, +219.2% E128_K8_I768, 12 layers, seq=1024 old: 136.89 ms, 7,480.3 tok/s new: 63.33 ms, 16,170.2 tok/s speedup: 2.16x, +116.2% E128_K8_I1408, 8 layers, seq=1024 old: 103.66 ms, 9,878.7 tok/s new: 52.81 ms, 19,391.2 tok/s speedup: 1.96x, +96.3% ``` For the default-ish `E=60, top_k=4` shape, I did not see a meaningful total-throughput improvement. The gain scales with routing pressure; high-expert / high-`top_k` configurations are where the old expanded-index route/combine path becomes expensive. ## Tests ```text LD_LIBRARY_PATH= uv run --no-sync python -m ruff check \ cosmos_framework/model/vfm/vlm/qwen3_vl_moe/moe.py \ cosmos_framework/model/vfm/vlm/qwen3_vl_moe/moe_test.py LD_LIBRARY_PATH= uv run --no-sync python -m cosmos_framework.model.vfm.vlm.qwen3_vl_moe.moe_test LD_LIBRARY_PATH= uv run --no-sync python -m cosmos_framework.model.vfm.vlm.qwen3_vl_moe.moe_bench \ --compare --num-tokens 256 --num-iters 10 --num-warmup 3 --no-compile --dtype fp32 LD_LIBRARY_PATH= uv run --no-sync python -m cosmos_framework.model.vfm.vlm.qwen3_vl_moe.moe_bench \ --compare --num-tokens 2048 --num-iters 20 --num-warmup 5 --no-compile --dtype bf16 LD_LIBRARY_PATH= uv run --no-sync python -m cosmos_framework.model.vfm.vlm.qwen3_vl_moe.moe_bench \ --num-tokens 1024 --num-iters 5 --num-warmup 2 --dtype bf16 --backward ``` Signed-off-by: Shivanjan Chakravorty <shivanjanc@nvidia.com> Co-authored-by: lfengad <liangf@nvidia.com>
1 parent 19a00f8 commit fa63e99

2 files changed

Lines changed: 30 additions & 19 deletions

File tree

cosmos_framework/model/vfm/vlm/qwen3_vl_moe/moe.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,14 @@ def forward(
104104
sentinel = torch.tensor([num_tokens], device=hidden_states.device) # for padding slots
105105
token_indices_ext = torch.cat([token_indices_sorted, sentinel])
106106
combined_indices = token_indices_ext[permuted_indices.long()]
107-
combined_indices = combined_indices.unsqueeze(-1).expand(-1, dim)
108107

109108
# Pad scores with a zero sentinel so padding slots contribute nothing
110109
scores_ext = torch.cat([topk_scores_sorted, topk_scores_sorted.new_zeros(1)])
111110
combined_scores = scores_ext[permuted_indices.long()] # [padded_len]
112111

113112
# Single gather (with a zero-padded sentinel row)
114113
input_padded = torch.cat([hidden_states, hidden_states.new_zeros(1, dim)])
115-
routed_input = input_padded.gather(dim=0, index=combined_indices)
114+
routed_input = input_padded.index_select(dim=0, index=combined_indices)
116115

117116
# Run experts
118117
routed_output = _run_experts_grouped_mm(
@@ -125,7 +124,7 @@ def forward(
125124
)
126125

127126
output_padded = torch.zeros_like(input_padded)
128-
output_padded.scatter_add_(dim=0, index=combined_indices, src=routed_output)
127+
output_padded.index_add_(dim=0, index=combined_indices, source=routed_output)
129128
return output_padded[:-1]
130129

131130
def _reorder_tokens(
@@ -220,9 +219,8 @@ def forward(
220219
assert weighted_output.dtype == hidden_states.dtype
221220
next_states.index_add_(0, token_idx, weighted_output)
222221
else:
223-
hidden_states = hidden_states.repeat(self.num_experts, 1) # [num_experts*num_tokens,hidden_size]
224-
hidden_states = hidden_states.view(
225-
self.num_experts, -1, self.hidden_size
222+
hidden_states = hidden_states.unsqueeze(0).expand(
223+
self.num_experts, -1, -1
226224
) # [num_experts,num_tokens,hidden_size]
227225
gate_up = torch.bmm(hidden_states, self.gate_up_proj) # [num_experts,num_tokens,2*moe_intermediate_size]
228226
gate, up = gate_up.chunk(

cosmos_framework/model/vfm/vlm/qwen3_vl_moe/moe_test.py

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,26 @@
1212
from cosmos_framework.model.vfm.vlm.qwen3_vl_moe.moe import create_text_experts
1313

1414

15-
def run_moe(mod: nn.Module, hidden_states: torch.Tensor, topk_scores: torch.Tensor, expert_indices: torch.Tensor):
15+
def run_moe(
16+
mod: nn.Module,
17+
hidden_states: torch.Tensor,
18+
topk_scores: torch.Tensor,
19+
expert_indices: torch.Tensor,
20+
num_tokens_per_expert: torch.Tensor,
21+
):
1622
num_warmup_iterations = 10
1723
num_timing_iterations = 100
1824

1925
for _ in range(num_warmup_iterations):
2026
with torch.no_grad():
21-
output = mod(hidden_states, topk_scores, expert_indices)
27+
output = mod(hidden_states, topk_scores, expert_indices, num_tokens_per_expert)
28+
torch.cuda.synchronize()
2229

2330
start_time = time.time()
2431
for _ in range(num_timing_iterations):
2532
with torch.no_grad():
26-
output = mod(hidden_states, topk_scores, expert_indices)
33+
output = mod(hidden_states, topk_scores, expert_indices, num_tokens_per_expert)
34+
torch.cuda.synchronize()
2735
end_time = time.time()
2836

2937
time_taken = (end_time - start_time) / num_timing_iterations
@@ -46,7 +54,7 @@ def main():
4654
control = create_text_experts(config, implementation_type="naive")
4755
exp = create_text_experts(config, implementation_type="grouped_mm")
4856

49-
control.init_weights()
57+
control.init_weights(torch.device("cpu"))
5058
exp.load_state_dict(control.state_dict())
5159

5260
control = control.to(device="cuda", dtype=torch.bfloat16)
@@ -58,31 +66,36 @@ def main():
5866
dtype=torch.bfloat16,
5967
device="cuda",
6068
)
61-
topk_scores = torch.randn(
69+
topk_scores = torch.rand(
6270
num_tokens,
6371
config.num_experts_per_tok,
6472
dtype=torch.bfloat16,
6573
device="cuda",
6674
)
6775
topk_scores = topk_scores / topk_scores.sum(dim=-1, keepdim=True)
68-
expert_indices = torch.randint(
69-
0,
70-
config.num_experts,
71-
(num_tokens, config.num_experts_per_tok),
72-
dtype=torch.int64,
73-
device="cuda",
76+
expert_indices = torch.stack(
77+
[torch.randperm(config.num_experts, device="cuda")[: config.num_experts_per_tok] for _ in range(num_tokens)]
78+
).to(torch.int64)
79+
num_tokens_per_expert = torch.histc(
80+
expert_indices.to(dtype=torch.int32).view(-1),
81+
bins=config.num_experts,
82+
min=0,
83+
max=config.num_experts - 1,
7484
)
7585

7686
print(
7787
f"hidden_states: {hidden_states.norm().detach().cpu().item()} {hidden_states.shape} {hidden_states.dtype} {hidden_states.device}"
7888
)
7989

80-
control_output, control_time_taken = run_moe(control, hidden_states, topk_scores, expert_indices)
81-
exp_output, exp_time_taken = run_moe(exp, hidden_states, topk_scores, expert_indices)
90+
control_output, control_time_taken = run_moe(
91+
control, hidden_states, topk_scores, expert_indices, num_tokens_per_expert
92+
)
93+
exp_output, exp_time_taken = run_moe(exp, hidden_states, topk_scores, expert_indices, num_tokens_per_expert)
8294

8395
diff = (control_output.detach().cpu() - exp_output.detach().cpu()).norm() / control_output.detach().cpu().norm()
8496
print(f"Diff: {diff}")
8597
print(f"Speedup: {control_time_taken / exp_time_taken}")
98+
torch.testing.assert_close(control_output, exp_output, rtol=1e-2, atol=1e-2)
8699

87100

88101
if __name__ == "__main__":

0 commit comments

Comments
 (0)