mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-28 23:07:09 +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>
112 lines
4.7 KiB
Python
112 lines
4.7 KiB
Python
"""fp8 on trn2: keep every ``float8_e4m3fn`` value inside the range of the hardware's e4m3 (issue #24).
|
|
|
|
trn2 has no e4m3fn. With ``--experimental-unsafe-fp8e4m3fn-as-fp8e4m3`` the compiler accepts ``float8_e4m3fn``
|
|
tensors and reads their bits as IEEE-style e4m3 (bias 7, max 240, exponent field 15 = inf/NaN). Both formats decode
|
|
identically whenever the exponent field is below 15, i.e. for |x| < 256. So on Neuron:
|
|
|
|
* activations / KV records quantize to ``FP8_MAX`` (224, as vllm-neuron#71) instead of 448. With power-of-two
|
|
(ue8m0) or ``amax / max`` scales the scale just doubles, so dequantized values are unchanged except in the lowest
|
|
subnormal binade;
|
|
* checkpoint fp8 weights (full ±448 range) are rescaled once at load: values halved, scales doubled
|
|
(``rescale_fp8_weights``), exact except the lowest subnormal bit.
|
|
|
|
``DEEPINFER_NEURON_FP8_MAX=448`` restores the e4m3fn range (host-mode debugging only: the device cannot decode it).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shlex
|
|
|
|
import torch
|
|
|
|
FP8_MAX = float(os.environ.get("DEEPINFER_NEURON_FP8_MAX", "224"))
|
|
_HW_E4M3_FLAG = "--experimental-unsafe-fp8e4m3fn-as-fp8e4m3"
|
|
|
|
|
|
def configure_compiler() -> None:
|
|
"""Let neuronx-cc (eager and torch.compile) take float8_e4m3fn tensors as the hardware e4m3."""
|
|
if FP8_MAX > 240.0:
|
|
return
|
|
os.environ.setdefault("UNSAFE_FP8FNCAST", "1")
|
|
flags = os.environ.get("NEURON_CC_FLAGS", "")
|
|
if _HW_E4M3_FLAG not in flags:
|
|
# The option belongs to hlo2tensorizer; neuronx-cc rejects it as a top-level flag (NCC_EARG002). All
|
|
# hlo2tensorizer options must share one --internal-hlo2tensorizer-options (a second one replaces the first),
|
|
# so append to an existing one if the user set it.
|
|
key = "--internal-hlo2tensorizer-options="
|
|
if key in flags:
|
|
head, _, rest = flags.partition(key)
|
|
value, sep, tail = rest.partition(" --")
|
|
value = value.strip("'\"")
|
|
flags = f"{head}{key}'{value} {_HW_E4M3_FLAG}'" + (f" --{tail}" if sep else "")
|
|
else:
|
|
flags = f"{flags} {key}{_HW_E4M3_FLAG}".strip()
|
|
os.environ["NEURON_CC_FLAGS"] = flags
|
|
|
|
|
|
def halve_e4m3fn_bytes(b: torch.Tensor) -> torch.Tensor:
|
|
"""``e4m3fn(x / 2)`` from the bits of ``x`` (uint8), round-to-nearest-even; integer ops only, any device."""
|
|
v = b.to(torch.int16)
|
|
sign = v & 0x80
|
|
mag = v & 0x7F
|
|
exp = mag >> 3
|
|
man = mag & 7
|
|
# exponent >= 2: decrement the exponent. exponent 1 -> subnormal (8 + m) / 2; subnormal m -> m / 2 (RNE).
|
|
sub_in = torch.where(exp == 1, man + 8, man)
|
|
half = sub_in >> 1
|
|
rne = half + ((sub_in & 1) & (half & 1))
|
|
out = torch.where(exp >= 2, mag - 8, torch.where(exp <= 1, rne, mag))
|
|
out = torch.where(mag == 0x7F, mag, out) # NaN stays NaN
|
|
return (out | sign).to(torch.uint8)
|
|
|
|
|
|
_CHUNK_BYTES = 1 << 26
|
|
|
|
|
|
def _halve_inplace(data: torch.Tensor) -> None:
|
|
flat = data.view(-1)
|
|
for start in range(0, flat.numel(), _CHUNK_BYTES): # bounded int16 temporaries (Engram tables are ~200 GB)
|
|
chunk = flat[start : start + _CHUNK_BYTES]
|
|
chunk.copy_(halve_e4m3fn_bytes(chunk))
|
|
|
|
|
|
def _double_scale(sf: torch.Tensor) -> None:
|
|
if sf.dtype in (torch.float32, torch.bfloat16, torch.float16):
|
|
sf.mul_(2)
|
|
return
|
|
# ue8m0 exponents (float8_e8m0fnu, or bytes packed into int words): exponent + 1.
|
|
sf_bytes = sf.view(torch.uint8)
|
|
assert not bool((sf_bytes == 255).any()), "ue8m0 scale overflow"
|
|
sf_bytes.add_(1)
|
|
|
|
|
|
def rescale_fp8_weights(model: torch.nn.Module) -> int:
|
|
"""Halve every fp8 weight (and double its scale) so it fits the hardware e4m3. Returns the count rescaled.
|
|
|
|
Run once, on the checkpoint parameters right after loading (before runtime adaptation merges or repacks them).
|
|
Parameters sharing storage (tied weights) are rescaled once.
|
|
"""
|
|
if FP8_MAX > 240.0:
|
|
return 0
|
|
by_storage: dict[int, torch.nn.Parameter] = {}
|
|
for p in model.parameters():
|
|
if p.dtype != torch.float8_e4m3fn or p.numel() == 0:
|
|
continue
|
|
ptr = p.untyped_storage().data_ptr()
|
|
if getattr(p, "sf", None) is not None or ptr not in by_storage:
|
|
by_storage[ptr] = p # prefer the copy that carries the scale (tied aliases may not)
|
|
count = 0
|
|
for p in by_storage.values():
|
|
sf = getattr(p, "sf", None)
|
|
assert sf is not None, "fp8 weight without a scale cannot be rescaled"
|
|
assert p.is_contiguous()
|
|
_halve_inplace(p.data.view(torch.uint8))
|
|
_double_scale(sf.data if isinstance(sf, torch.nn.Parameter) else sf)
|
|
count += 1
|
|
return count
|
|
|
|
|
|
def describe() -> str:
|
|
return f"fp8 max {FP8_MAX:g}, NEURON_CC_FLAGS={shlex.quote(os.environ.get('NEURON_CC_FLAGS', ''))}"
|