|
| 1 | +# SPDX-License-Identifier: Apache-2.0 |
| 2 | +"""Pure-PyTorch gathered-expert MoE combine for silu + FP8-per-channel weights. |
| 3 | +
|
| 4 | +Replaces the Habana ``mixture_of_experts`` combine (a fixed per-layer launch |
| 5 | +pipeline) with a leaner active-expert gather + GEMM + weighted-reduce path, |
| 6 | +mirroring ``vllm_gaudi.ops.hpu_fused_moe._gather_swigluoai_moe`` but for a silu |
| 7 | +gated activation and the FP8 per-channel weight layout |
| 8 | +(``extension/ops.py:fp8_channel_moe_prepare_weights``). |
| 9 | +
|
| 10 | +Only ``g = min(E_local, tokens * K)`` distinct routed experts are read from HBM |
| 11 | +and computed (<= 8 at BS=1 for K=8), instead of the Habana op's fixed stage |
| 12 | +pipeline. The gathered count is static (no ``torch.nonzero``/host branch), so the |
| 13 | +path captures cleanly into a compiled HPU graph. |
| 14 | +
|
| 15 | +Numeric fidelity: ``x`` and the weights are FP8; ``x`` is quantized the same way |
| 16 | +the op does (``dynamic_quant``), everything is dequantized to fp32, and the two |
| 17 | +GEMMs + silu + weighted sum accumulate in fp32, rounding to the output dtype only |
| 18 | +at the end. Exact bit-equality with the black-box op is NOT expected; correctness |
| 19 | +is assessed via an FP8-ULP bar. |
| 20 | +""" |
| 21 | +from __future__ import annotations |
| 22 | + |
| 23 | +import torch |
| 24 | + |
| 25 | + |
| 26 | +def _dynamic_quant(data): |
| 27 | + # import lazily to avoid pulling heavy deps at module import |
| 28 | + from vllm_gaudi.extension.ops import dynamic_quant |
| 29 | + return dynamic_quant(data) |
| 30 | + |
| 31 | + |
| 32 | +def gather_silu_fp8_moe(layer, x, topk_ids, topk_weights, activation="silu"): |
| 33 | + """x [T,H] bf16 -> MoE output [T,H] bf16. |
| 34 | +
|
| 35 | + topk_ids [T,K] int64 (global expert ids), topk_weights [T,K] bf16. |
| 36 | + Mirrors the Habana op's data flow: fp8-quantize x, per-token weighted sum |
| 37 | + over the routed experts, each expert computed as silu(w13 x) w2. |
| 38 | + """ |
| 39 | + assert activation == "silu", f"custom combine supports silu, got {activation}" |
| 40 | + |
| 41 | + T, H = x.shape |
| 42 | + K = topk_ids.shape[-1] |
| 43 | + w13 = layer.w13_weight # [E, 2I, H] fp8 |
| 44 | + w2 = layer.w2_weight # [E, H, I] fp8 |
| 45 | + s13 = layer.w13_weight_scale_inv # [E, 2I] |
| 46 | + s2 = layer.w2_weight_scale_inv # [E, H] |
| 47 | + E = w13.shape[0] |
| 48 | + I = w13.shape[1] // 2 |
| 49 | + |
| 50 | + # ---- FP8-quantize x per token (match the op's input quantization) ---- |
| 51 | + x_fp8, x_scale = _dynamic_quant(x) # x_fp8 [T,H], x_scale [T,1] f32 |
| 52 | + x_f = x_fp8.to(torch.float32) * x_scale # [T,H] f32 |
| 53 | + |
| 54 | + # ---- per-token expert combine weights (scatter over E) ---- |
| 55 | + # topk_ids are GLOBAL expert ids; remap to this rank's local ids and mask |
| 56 | + # experts owned by other EP ranks (they contribute +0.0 to this rank's |
| 57 | + # partial, which is then reduce-scattered by the runner). At TP=1 |
| 58 | + # ep_rank=0 -> experts_min=0, identical to the unremapped path. |
| 59 | + experts_min = int(layer.moe_config.ep_rank * layer.local_num_experts) |
| 60 | + local_ids = topk_ids - experts_min # [T,K] |
| 61 | + in_range = (local_ids >= 0) & (local_ids < E) |
| 62 | + safe_ids = torch.where(in_range, local_ids, torch.zeros_like(local_ids)) |
| 63 | + safe_w = torch.where(in_range, topk_weights, torch.zeros_like(topk_weights)).to(torch.float32) |
| 64 | + gate_w = x.new_zeros(T, E, dtype=torch.float32) |
| 65 | + gate_w.scatter_add_(1, safe_ids, safe_w) # [T,E] f32 |
| 66 | + |
| 67 | + # ---- static gathered-expert count (>= # distinct hit experts) ---- |
| 68 | + # The number of distinct routed experts is provably <= tokens * K, so with |
| 69 | + # g = min(E, tokens*K) every real hit is included and the extra (zero-weight) |
| 70 | + # padding experts contribute exactly +0.0 to the weighted sum. Keeping `g` |
| 71 | + # static (no torch.nonzero, no `if G == 0`) lets this capture cleanly into a |
| 72 | + # compiled HPU graph (mirrors _gather_swigluoai_moe). |
| 73 | + g = min(E, T * K) |
| 74 | + hit = (gate_w != 0).float().sum(0) # [E] |
| 75 | + gather_ids = torch.topk(hit, g, sorted=False).indices # [G] |
| 76 | + gather_ids, _ = torch.sort(gather_ids) # ascending ids |
| 77 | + |
| 78 | + # ---- gather only the active experts' weights + per-channel scales ---- |
| 79 | + w13_g = w13.index_select(0, gather_ids) # [G, 2I, H] fp8 |
| 80 | + w2_g = w2.index_select(0, gather_ids) # [G, H, I] fp8 |
| 81 | + s13_g = s13.index_select(0, gather_ids) # [G, 2I] |
| 82 | + s2_g = s2.index_select(0, gather_ids) # [G, H] |
| 83 | + w13_f = w13_g.to(torch.float32) * s13_g.unsqueeze(-1) # [G, 2I, H] f32 |
| 84 | + w2_f = w2_g.to(torch.float32) * s2_g.unsqueeze(-1) # [G, H, I] f32 |
| 85 | + |
| 86 | + # ---- per-expert MLP ---- |
| 87 | + xe = x_f.unsqueeze(0).expand(g, T, H) # [G, T, H] |
| 88 | + h = torch.bmm(xe, w13_f.transpose(1, 2)) # [G, T, 2I] f32 |
| 89 | + gate, up = h[..., :I], h[..., I:] |
| 90 | + act = gate * torch.sigmoid(gate) * up # silu(gate)*up, [G,T,I] |
| 91 | + y = torch.bmm(act, w2_f.transpose(1, 2)) # [G, T, H] f32 |
| 92 | + |
| 93 | + # ---- weighted sum over experts -> [T, H] ---- |
| 94 | + gate_wg = gate_w.index_select(1, gather_ids).t() # [G, T] |
| 95 | + out = (y * gate_wg.unsqueeze(-1)).sum(0) # [T, H] f32 |
| 96 | + return out.to(x.dtype) |
0 commit comments