mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-31 00:36:20 +08:00
- 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>
128 lines
5.6 KiB
Python
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))
|