whn09 02e099fff8 Neuron: static decode graphs, static-shape MoE, trn2 e4m3 fp8 (issues #9, #12, #24)
- NeuronGraphRunner: the CudaGraphRunner contract over torch.compile(dynamic=False), one graph per
  batch size (DEEPINFER_ENFORCE_EAGER=0, backend DEEPINFER_NEURON_COMPILE_BACKEND). Layer cache views are
  re-sliced from their root allocations inside the graph, so AOT never regenerates aliased mutated
  inputs with as_strided.
- Decode MoE with fixed shapes: packed all-gather, dense local experts, fp32 reduce-scatter
  (neuron_ops/static_moe.py, deep_gemm.m_grouped_fp8_fp4_gemm_nt_shared_a).
- Graph-break and compile fixes: hide torch.cuda during compile, torch.device equality for FX codegen,
  hash-based force_random routing, Engram hash/fetch on the host (exact int64), env vars resolved before
  tracing, Movable annotation, no .item()/nonzero/as_strided writes on the decode path, KV stores route
  padding slots to a scratch block kept past the pool, native-dtype gather under compile.
- fp8: trn2 reads float8_e4m3fn as IEEE e4m3 (max 240). Activations/KV quantize to 224
  (DEEPINFER_NEURON_FP8_MAX), checkpoint fp8 weights are rescaled at load (bytes / 2, ue8m0 + 1), and
  neuronx-cc gets --experimental-unsafe-fp8e4m3fn-as-fp8e4m3 via --internal-hlo2tensorizer-options.
  The uint8-carrier backend (neuron_ops/neuron_compile.py, "deepinfer_neuron") stays as a fallback.
- Tests: tests/neuron/compile; run_all.sh runs kernel unit tests at the e4m3fn range (448) and the
  end-to-end stages at 224. Docs: vllm-neuron PR #71 analysis and compile notes.

Host: tiny model 9/9 vs golden (all prompts) eager, compiled path (aot_eager) matches eager; CPU suite green.
Device: the whole decode graph still crashes neuronx-cc (NCC_IINL902, being bisected).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:59:37 +08:00

128 lines
5.6 KiB
Python

"""Static-shape expert parallelism for the Neuron decode graphs (issue #12).
The dispatch -> m_grouped GEMM -> combine path sizes everything by data (how many tokens each rank receives, the
psum layout of each expert), which costs host syncs and recompiles. Here every shape is fixed by the batch size:
1. pad the local tokens to ``max_num_decoding_tokens`` rows and all-gather tokens, routing and gate weights
over the EP group (one packed uint8 collective);
2. run each local expert densely over all gathered rows, the gate weight of non-routed rows being 0;
3. reduce-scatter the fp32 sum back to the owning rank and add the shared expert.
Decode batches are small, so computing every local expert over the gathered rows costs little next to the
weight traffic, and the step traces into one graph with no data-dependent shapes.
"""
from __future__ import annotations
import os
from typing import TYPE_CHECKING
import deep_gemm
import torch
import torch.distributed._functional_collectives as funcol
from tile_kernels.quant import swiglu_forward_and_per_token_cast
from deepinfer.model_executor.layers.linear import get_fp8_cast_params, get_fp8_casting_granularity
if TYPE_CHECKING:
from deepinfer.model_executor.models.v4_modules.mlp import DeepseekMoE
# gloo (host mode) has no reduce-scatter.
_HOST = os.environ.get("DEEPINFER_NEURON_DEVICE", "neuron") == "cpu"
_ALIGN = 16
def _as_rows(t: torch.Tensor, rows: int) -> torch.Tensor:
return t.contiguous().view(rows, -1).view(torch.uint8)
def _pack(parts: list[torch.Tensor], cap: int) -> tuple[torch.Tensor, list[tuple[int, int]]]:
"""Byte rows ``[cap, W]``; each part starts at a 16-byte aligned column. Rows past the tokens are 0xFF so the
padded routing reads as -1 (int32) / NaN (fp32), i.e. selects no expert."""
num_tokens = parts[0].shape[0]
spans, cols, width = [], [], 0
for part in parts:
b = _as_rows(part, num_tokens)
spans.append((width, b.shape[1]))
pad = -b.shape[1] % _ALIGN
cols.append(torch.nn.functional.pad(b, (0, pad)) if pad else b)
width += b.shape[1] + pad
packed = torch.cat(cols, dim=1)
if cap > num_tokens:
packed = torch.cat([packed, packed.new_full((cap - num_tokens, width), 0xFF)], dim=0)
return packed, spans
def _unpack(packed: torch.Tensor, span: tuple[int, int], like: torch.Tensor) -> torch.Tensor:
start, nbytes = span
rows = packed.shape[0]
return packed[:, start : start + nbytes].contiguous().view(like.dtype).view(rows, *like.shape[1:])
def _all_gather(t: torch.Tensor, group: torch.distributed.ProcessGroup, size: int) -> torch.Tensor:
if size == 1:
return t
return funcol.wait_tensor(funcol.all_gather_tensor(t, 0, group))
def _reduce_scatter(t: torch.Tensor, group: torch.distributed.ProcessGroup, size: int, rank: int) -> torch.Tensor:
if size == 1:
return t
if _HOST:
n = t.shape[0] // size
return funcol.wait_tensor(funcol.all_reduce(t, "sum", group)).narrow(0, rank * n, n)
return funcol.wait_tensor(funcol.reduce_scatter_tensor(t, "sum", 0, group))
def static_moe_forward(
moe: DeepseekMoE,
hidden_states: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
topk_idx: torch.Tensor,
topk_weights: torch.Tensor,
) -> torch.Tensor:
group, size, rank = moe.ep_group, moe.ep_size, moe.ep_rank
num_local = moe.num_local_physical_experts
x_parts = list(hidden_states) if moe.use_fp8_act else [hidden_states]
num_tokens = x_parts[0].shape[0]
cap = max(moe.max_num_decoding_tokens, num_tokens)
parts = x_parts + [topk_idx.to(torch.int32), topk_weights.to(torch.float32)]
packed, spans = _pack(parts, cap)
gathered = _all_gather(packed, group, size) # [size * cap, W]
g_parts = [_unpack(gathered, span, part) for span, part in zip(spans, parts)]
g_x = tuple(g_parts[:-2]) if moe.use_fp8_act else g_parts[0]
g_idx, g_w = g_parts[-2], g_parts[-1]
num_rows = g_idx.shape[0]
recipe_a = (1, get_fp8_casting_granularity())
recipe_b = (1, 32 if moe.use_fp4_weight else get_fp8_casting_granularity())
swiglu_kwargs = get_fp8_cast_params() if moe.use_fp8_act else {"fmt": "bf16", "num_per_channels": None}
gate_up_w = moe._weight_and_sf(moe.gate_up_proj_weight)
down_w = moe._weight_and_sf(moe.down_proj_weight)
acc = torch.zeros((num_rows, moe.hidden_size), dtype=torch.float32, device=g_idx.device)
for e in range(num_local):
hit = g_idx == rank * num_local + e
w_e = torch.where(hit, g_w, torch.zeros_like(g_w)).sum(-1)
gate_up = torch.empty((num_rows, moe.gate_up_proj_weight.shape[1]), dtype=torch.bfloat16, device=g_idx.device)
_expert_gemm(g_x, gate_up_w, gate_up, e, moe.use_fp8_act, recipe_a, recipe_b)
down_in = swiglu_forward_and_per_token_cast(
gate_up, **swiglu_kwargs, topk_weights=w_e, clamp_value=moe.swiglu_clamp_value
)
down = torch.empty((num_rows, moe.down_proj_weight.shape[1]), dtype=torch.bfloat16, device=g_idx.device)
_expert_gemm(down_in, down_w, down, e, moe.use_fp8_act, recipe_a, recipe_b)
acc = acc + down.float()
out = _reduce_scatter(acc, group, size, rank)[:num_tokens]
shared = moe.shared_forward(hidden_states)
if shared is not None:
out = out + shared.float()
return out.to(torch.bfloat16)
def _expert_gemm(a, b, d, e: int, fp8: bool, recipe_a, recipe_b) -> None: # noqa: ANN001
if fp8:
deep_gemm.m_grouped_fp8_fp4_gemm_nt_shared_a(a, b, d, e, recipe_a=recipe_a, recipe_b=recipe_b)
else:
d.copy_((a.float() @ b[e].float().transpose(0, 1)).to(d.dtype))