DeepInfer/deepinfer/worker/neuron_graph_runner.py
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

159 lines
7.6 KiB
Python

"""Static decode graphs on Neuron: the CudaGraphRunner contract, backed by torch.compile(backend="neuron").
One compiled graph per captured batch size, over persistent device metadata buffers (the same buffers the CUDA
runner captures over). Dynamo specializes on their shapes (dynamic=False), so each batch size is one NEFF set.
Replay copies the host metadata into the device buffers and calls the compiled forward; the host->device copy stays
outside the graph.
"""
from __future__ import annotations
import dataclasses
import math
from collections.abc import Callable
from typing import TYPE_CHECKING
import torch
from deepinfer import envs
from deepinfer.environ import _descriptors as _env_descriptors
from deepinfer.logger import init_logger
from deepinfer.model_executor.input_metadata import InputMetadata
from deepinfer.model_executor.model_output import CausalLMForwardOutput
from deepinfer.neuron_ops import neuron_compile # noqa: F401 (registers the "deepinfer_neuron" backend)
from deepinfer.neuron_ops.transfer_to_neuron import hide_cuda
from deepinfer.sequence import SequenceMetadata
from deepinfer.worker.cache_engine import GPUCacheInterface, KVCache
from deepinfer.worker.cuda_graph_runner import CudaGraphData, CudaGraphRunner
if TYPE_CHECKING:
from deepinfer.model_executor.models.deepseek import DeepseekForCausalLM
logger = init_logger(__name__)
class _GraphKVCaches:
"""The per-layer cache views, rebuilt from their root allocations inside the graph.
The layer caches are views of one KV buffer and one state buffer. Passed as graph inputs, they alias each other,
and AOT regenerates aliased mutated inputs with as_strided, which cannot be functionalized. Here only the two
roots are graph inputs; each layer's views are re-sliced from them (slice + view, no as_strided) on access.
"""
def __init__(self, kv_caches: GPUCacheInterface) -> None:
self.roots: list[torch.Tensor] = []
self.recipes: list[dict[str, tuple | None]] = []
for cache in kv_caches.cache_list:
recipe = {}
for f in dataclasses.fields(KVCache):
t = getattr(cache, f.name)
recipe[f.name] = None if t is None else self._recipe(t)
self.recipes.append(recipe)
def _recipe(self, t: torch.Tensor) -> tuple:
storage = t.untyped_storage()
for i, r in enumerate(self.roots):
if r.untyped_storage().data_ptr() == storage.data_ptr():
root = r
break
else:
# One flat uint8 tensor over the whole allocation (dtype views of it have no usable _base).
root = torch.empty(0, dtype=torch.uint8, device=t.device).set_(storage)
self.roots.append(root)
i = len(self.roots) - 1
rows, row_bytes = t.shape[0], t.stride(0) * t.element_size()
offset = t.storage_offset() * t.element_size()
assert offset + math.prod(t.shape[1:]) * t.element_size() <= row_bytes, "cache view must be a column slice of its allocation"
nbytes = math.prod(t.shape[1:]) * t.element_size()
recipe = (i, rows, row_bytes, offset, nbytes, tuple(t.shape[1:]), t.dtype)
rebuilt = self._build(root, recipe)
assert rebuilt.data_ptr() == t.data_ptr() and rebuilt.shape == t.shape and rebuilt.stride() == t.stride(), (
f"cache view not expressible as a row slice: {t.shape=} {t.stride()=}"
)
return recipe
@staticmethod
def _build(root: torch.Tensor, recipe: tuple) -> torch.Tensor:
_, rows, row_bytes, offset, nbytes, tail, dtype = recipe
esize = torch.empty((), dtype=dtype).element_size()
flat = root[: rows * row_bytes]
view = flat.view(rows, row_bytes)[:, offset : offset + nbytes].view(rows, *tail[:-1], tail[-1] * esize)
return view if dtype == torch.uint8 else view.view(dtype)
def __getitem__(self, layer_idx: int) -> KVCache:
tensors = {}
for name, recipe in self.recipes[layer_idx].items():
tensors[name] = None if recipe is None else self._build(self.roots[recipe[0]], recipe)
return KVCache(**tensors)
def is_valid_layeridx(self, layer_idx: int) -> bool:
return layer_idx < len(self.recipes)
class NeuronGraphRunner(CudaGraphRunner):
def __init__(self, max_batch_size: int, input_on_device: bool = False) -> None:
super().__init__(max_batch_size, input_on_device)
self.compiled: dict[int, Callable[..., CausalLMForwardOutput]] = {}
self.model: DeepseekForCausalLM | None = None
self.kv_caches: GPUCacheInterface | None = None
def capture(
self,
model: DeepseekForCausalLM,
kv_caches: GPUCacheInterface,
prepare_input_fn: Callable[[list[SequenceMetadata]], InputMetadata],
padding_seq_metadata: SequenceMetadata,
) -> None:
self.destroy()
self.model, self.kv_caches = model, kv_caches
# Resolve every env var now: the lazy descriptor (lock + os.getenv) does not trace.
for name in _env_descriptors:
getattr(envs, name)
backend, fullgraph = envs.DEEPINFER_NEURON_COMPILE_BACKEND, envs.DEEPINFER_NEURON_FULLGRAPH
logger.info(f"Neuron decode graphs: backend={backend} fullgraph={fullgraph} batch_sizes={self.batch_sizes}")
self.padding_seq_metadata_list = [padding_seq_metadata] * self.max_batch_size
input_metadata_source_full = prepare_input_fn(self.padding_seq_metadata_list)
input_metadata_device_full = input_metadata_source_full.recursive_fn(lambda x: x.cuda(non_blocking=True))
graph_caches = _GraphKVCaches(kv_caches)
def forward(input_metadata: InputMetadata) -> CausalLMForwardOutput:
return model(kv_caches=graph_caches, input_metadata=input_metadata)
self.graphs = {}
for batch_size in reversed(self.batch_sizes):
def _cut(x: torch.Tensor, _bs: int = batch_size) -> torch.Tensor:
assert x.shape[0] == self.max_batch_size
return x[:_bs]
input_metadata_source = input_metadata_source_full.recursive_fn(_cut)
input_metadata_device = input_metadata_device_full.recursive_fn(_cut)
compiled = torch.compile(forward, backend=backend, fullgraph=fullgraph, dynamic=False)
# First call compiles (and runs) this batch size's graph.
input_metadata_device.recursive_copy_(input_metadata_source)
with hide_cuda():
compiled(input_metadata_device)
for name in input_metadata_source.get_clean_list():
setattr(input_metadata_source, name, None)
setattr(input_metadata_device, name, None)
self.compiled[batch_size] = compiled
self.graphs[batch_size] = CudaGraphData(None, None, None, input_metadata_source, input_metadata_device)
def replay(self, cuda_graph_data: CudaGraphData) -> CausalLMForwardOutput:
input_metadata_device = cuda_graph_data.input_metadata_device
# Forward recomputes these (and stores them on the metadata); reset them to the state the graph was traced in.
for name in input_metadata_device.get_clean_list():
setattr(input_metadata_device, name, None)
if not self.input_on_device:
input_metadata_device.recursive_copy_(cuda_graph_data.input_metadata_source)
batch_size = next(bs for bs, data in self.graphs.items() if data is cuda_graph_data)
with hide_cuda():
return self.compiled[batch_size](input_metadata_device)
def destroy(self) -> None:
super().destroy()
self.compiled = {}
self.model = self.kv_caches = None