mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-30 15:57:20 +08:00
`DEEPINFER_ENABLE_FLAGGEMS=1` turns it on. Verified on all 8 DP ranks: exit=0, 8/8 ranks agreeing, no kernel fallbacks, `hard failures: none`, correct answers. 19 ops actually dispatched, 152 call sites, 43 registrations from 37 requested names. The default is `flag_gems.only_enable(include=[...])`, not `enable()`, and that is the whole design: * allow-list -- every intermediate state is safe (unproven ops stay on PyTorch, which is correct by construction); an incomplete list costs throughput, not correctness. * deny-list -- I tried it first. Five full engine runs, one new defect each: `cat` (e8m0 dtype), `mul` (broadcast shape), `topk` (compile blowup), `rms_norm` (signature), `mm` (compile assert). The method requires being bitten before it can exclude, and every state along the way is unsafe. That matches the instruction this was built to: use what works, leave the rest. Where it is called is not incidental. `import flag_gems` initialises a CUDA context -- measured, `torch.cuda.is_initialized()` flips on the import alone, before `enable()`. A `sitecustomize` injector therefore put all 8 ranks on device 0 and NCCL refused with "Multiple Ranks are using the same GPU". It now runs inside `create_llm`, after `setup_cuda_visible_devices`, which is the same hazard already documented there for the profiler. Defects found and documented rather than worked around, because each one is worth knowing: * `mul` returns the *first operand's shape* when broadcasting expands it: (1,64,8)*(4,1,8) gives (1,64,8), and (64,1)*(1,128) gives (64,1). Silent, no error, fewer elements computed. It made `engram_hash_ref` disagree with PyTorch during a real run. add/sub/div/maximum/minimum all broadcast correctly, so it is mul's own bug. * `cumsum` drifts by up to 1.0 over a 512-element bf16 row (tolerance ~0.17) -- bf16 accumulation. * `topk`'s compile time explodes with k: 4.8 s at k=8, 19 s at k=64, >180 s at k=512, and the sampler asks for 4096. py-spy put all 8 ranks at 100% CPU inside `make_llir` with every GPU idle. * `cat` raises `KeyError: 'float8_e8m0fnu'` on the model's UE8M0 scale tensors during weight load. * `mm` passes every isolated probe yet fails inside the engine at `general_mm`; trigger unknown, so GEMMs stay on cuBLAS, which is faster anyway. Two methodological errors in my own measurements, both fixed and both recorded: 1. After `enable()`, `torch.mul` *is* `flag_gems.mul`, so the first version of the mul A/B compared FlagGems with itself and reported a perfect score. All references are now computed before enable. 2. "ok" did not mean FlagGems ran. `torch.bitwise_xor` has no FlagGems registration at all, so that "match" was PyTorch against PyTorch. `screen_ops.py` now checks registration first and reports such cases as `skip` rather than pass. Name mismatch is its own trap: FlagGems registers `add` bare but `eq.Scalar`, `where.self`, `repeat_interleave.self_int`, `_softmax` (not `softmax`), `div.true_divide`. Asking for `softmax` silently registers nothing. The bootstrap now expands names against `flag_gems._FULL_CONFIG` and warns on anything unresolved -- which is how `softmax` was found to need `_softmax`. Output is not token-identical to the no-FlagGems build (diverges at the first token), but two runs of the same FlagGems config are bit-identical, so the difference is Triton-versus-eager accumulation order, not nondeterminism. Answers stay correct and coherent.
229 lines
9.2 KiB
Python
229 lines
9.2 KiB
Python
"""Screen FlagGems ops for the defect class that broke `mul`, so the exclusion list is evidence-based.
|
|
|
|
`mul` returns the *first operand's shape* whenever broadcasting would expand it:
|
|
|
|
(1,64,8) * (4,1,8) -> FlagGems (1,64,8), PyTorch (4,64,8)
|
|
(64,1) * (1,128) -> FlagGems (64,1), PyTorch (64,128)
|
|
|
|
Silent, no error. It broke `engram_hash_ref` inside the engine, and it is not dtype-specific.
|
|
|
|
The traceback went through `flag_gems/utils/pointwise_dynamic.py` and `shape_utils.broadcast_shapes`,
|
|
which is shared by every pointwise op -- so the question is which *other* ops inherit it. This script
|
|
answers that: each op is run twice, same-shape and broadcasting, and the reference is always taken
|
|
before `enable()` so the comparison cannot be FlagGems-against-itself.
|
|
|
|
Output is a table, which is what the exclusion list in `deepinfer/flaggems_bootstrap.py` is built
|
|
from.
|
|
|
|
Run: PYTHONPATH=. python scripts/flaggems_port/screen_ops.py
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
import torch
|
|
|
|
DEV = "cuda"
|
|
|
|
|
|
@dataclass
|
|
class Case:
|
|
op: str
|
|
label: str
|
|
run: Callable[[], Any]
|
|
ref: Any
|
|
|
|
|
|
def ref_of(fn: Callable[[], Any]) -> Any:
|
|
out = fn()
|
|
if isinstance(out, torch.Tensor):
|
|
return out.clone()
|
|
if isinstance(out, (tuple, list)):
|
|
return type(out)(t.clone() if isinstance(t, torch.Tensor) else t for t in out)
|
|
return out
|
|
|
|
|
|
def same(ref: Any, got: Any) -> tuple[bool, str]:
|
|
if isinstance(ref, torch.Tensor):
|
|
if not isinstance(got, torch.Tensor):
|
|
return False, f"type {type(got).__name__}"
|
|
if ref.shape != got.shape:
|
|
return False, f"shape {tuple(ref.shape)} -> {tuple(got.shape)}"
|
|
if ref.dtype != got.dtype:
|
|
return False, f"dtype {ref.dtype} -> {got.dtype}"
|
|
if ref.dtype.is_floating_point:
|
|
d = (ref.float() - got.float()).abs().max().item()
|
|
s = ref.float().abs().max().item()
|
|
tol = 2.0**-8 * max(s, 1e-9) * 2
|
|
return (d <= tol, f"max diff {d:.2e}")
|
|
eq = torch.equal(ref, got)
|
|
if eq:
|
|
return True, "exact"
|
|
return False, f"{(ref != got).sum().item()}/{ref.numel()} wrong"
|
|
if isinstance(ref, (tuple, list)):
|
|
for r, g in zip(ref, got, strict=False):
|
|
ok, msg = same(r, g)
|
|
if not ok:
|
|
return False, msg
|
|
return True, "exact"
|
|
return ref == got, ""
|
|
|
|
|
|
def build_cases() -> list[Case]:
|
|
torch.manual_seed(0)
|
|
b = 64
|
|
n = 8
|
|
|
|
# (1, b, n) vs (4, 1, n): the exact shape pattern that broke engram_hash.
|
|
x3 = torch.randn((1, b, n), device=DEV, dtype=torch.bfloat16)
|
|
y3 = torch.randn((4, 1, n), device=DEV, dtype=torch.bfloat16)
|
|
i3 = torch.randint(0, 1000, (1, b, n), device=DEV, dtype=torch.int64)
|
|
j3 = torch.randint(1, 100, (4, 1, n), device=DEV, dtype=torch.int64)
|
|
|
|
# (b, 1) vs (1, m): the most common broadcast in transformer code (masks, scales).
|
|
x2 = torch.randn((b, 1), device=DEV, dtype=torch.bfloat16)
|
|
y2 = torch.randn((1, 128), device=DEV, dtype=torch.bfloat16)
|
|
|
|
xs = torch.randn((256, 512), device=DEV, dtype=torch.bfloat16)
|
|
ys = torch.randn((256, 512), device=DEV, dtype=torch.bfloat16)
|
|
|
|
cases: list[Case] = []
|
|
|
|
def add(name: str, label: str, fn: Callable[[], Any]) -> None:
|
|
cases.append(Case(name, label, fn, ref_of(fn)))
|
|
|
|
# binary pointwise, same shape (control: isolates broadcast as the cause)
|
|
add("add", "same-shape", lambda: torch.add(xs, ys))
|
|
add("mul", "same-shape", lambda: torch.mul(xs, ys))
|
|
add("sub", "same-shape", lambda: torch.sub(xs, ys))
|
|
add("div", "same-shape", lambda: torch.div(xs, ys))
|
|
add("maximum", "same-shape", lambda: torch.maximum(xs, ys))
|
|
add("minimum", "same-shape", lambda: torch.minimum(xs, ys))
|
|
|
|
# binary pointwise, broadcasting (the suspect class)
|
|
add("add", "broadcast 3d", lambda: torch.add(x3, y3))
|
|
add("mul", "broadcast 3d", lambda: torch.mul(x3, y3))
|
|
add("sub", "broadcast 3d", lambda: torch.sub(x3, y3))
|
|
add("div", "broadcast 3d", lambda: torch.div(x3, y3))
|
|
add("maximum", "broadcast 3d", lambda: torch.maximum(x3, y3))
|
|
add("minimum", "broadcast 3d", lambda: torch.minimum(x3, y3))
|
|
add("add", "broadcast 2d", lambda: torch.add(x2, y2))
|
|
add("mul", "broadcast 2d", lambda: torch.mul(x2, y2))
|
|
add("sub", "broadcast 2d", lambda: torch.sub(x2, y2))
|
|
|
|
# integer bitwise / remainder: what engram_hash also used
|
|
add("bitwise_xor_", "broadcast int64", lambda: i3.bitwise_xor(j3))
|
|
add("remainder", "broadcast int64", lambda: torch.remainder(i3, j3))
|
|
|
|
# scalar variants: same code path, different signature
|
|
add("mul", "scalar", lambda: torch.mul(xs, 3.0))
|
|
add("add", "scalar", lambda: torch.add(xs, 3.0))
|
|
|
|
# unary: should not involve broadcasting at all
|
|
add("silu", "unary", lambda: torch.nn.functional.silu(xs))
|
|
add("gelu", "unary", lambda: torch.nn.functional.gelu(xs))
|
|
add("abs", "unary", lambda: torch.abs(xs))
|
|
|
|
# reductions
|
|
add("softmax", "last dim", lambda: torch.softmax(xs, dim=-1))
|
|
add("amax", "last dim", lambda: torch.amax(xs, dim=-1))
|
|
add("sum", "last dim", lambda: torch.sum(xs, dim=-1))
|
|
|
|
# normalisation; guarded because rms_norm is only in newer torch
|
|
if hasattr(torch.nn.functional, "rms_norm"):
|
|
add("rms_norm", "1x512", lambda: torch.nn.functional.rms_norm(xs, (512,)))
|
|
|
|
# more pointwise / reduction candidates for an allow-list
|
|
add("exp", "unary", lambda: torch.exp(xs))
|
|
add("sigmoid", "unary", lambda: torch.sigmoid(xs))
|
|
add("rsqrt", "unary", lambda: torch.rsqrt(xs.abs() + 1))
|
|
add("neg", "unary", lambda: torch.neg(xs))
|
|
add("relu", "unary", lambda: torch.relu(xs))
|
|
add("sqr", "unary", lambda: torch.square(xs))
|
|
add("clamp", "min/max", lambda: torch.clamp(xs, -0.5, 0.5))
|
|
add("pow", "scalar", lambda: torch.pow(xs, 2))
|
|
add("reciprocal", "unary", lambda: torch.reciprocal(xs.abs() + 1))
|
|
add("mean", "last dim", lambda: torch.mean(xs, dim=-1))
|
|
add("cumsum", "last dim", lambda: torch.cumsum(xs, dim=-1))
|
|
add("masked_fill", "scalar", lambda: xs.masked_fill(xs > 0, -1.0))
|
|
add("where", "self", lambda: torch.where(xs > 0, xs, -xs))
|
|
add("eq", "scalar", lambda: torch.eq(xs, 0.0))
|
|
add("lt", "scalar", lambda: torch.lt(xs, 0.0))
|
|
add("ge", "scalar", lambda: torch.ge(xs, 0.0))
|
|
add("nonzero", "unary", lambda: torch.nonzero(xs > 0.5))
|
|
add("gather", "dim0", lambda: torch.gather(xs, 0, torch.zeros((8, 512), device=DEV, dtype=torch.int64)))
|
|
add("index_select", "dim0", lambda: torch.index_select(xs, 0, torch.arange(8, device=DEV)))
|
|
add("sort", "last dim", lambda: torch.sort(xs, dim=-1).values)
|
|
add("sin", "unary", lambda: torch.sin(xs))
|
|
add("cos", "unary", lambda: torch.cos(xs))
|
|
add("sqrt", "unary", lambda: torch.sqrt(xs.abs()))
|
|
add("bitwise_and", "scalar", lambda: torch.bitwise_and(i3, 255))
|
|
add("bincount", "int", lambda: torch.bincount(torch.zeros(64, device=DEV, dtype=torch.int64), minlength=8))
|
|
|
|
# shape ops
|
|
add("cat", "2 tensors", lambda: torch.cat([xs, xs], dim=0))
|
|
add("stack", "2 tensors", lambda: torch.stack([xs, xs], dim=0))
|
|
add("repeat_interleave", "self_int 2", lambda: torch.repeat_interleave(xs, 2, dim=0))
|
|
|
|
return cases
|
|
|
|
|
|
def main() -> int:
|
|
cases = build_cases() # references captured here, before enable()
|
|
import flag_gems
|
|
|
|
flag_gems.enable()
|
|
registered = sorted(flag_gems.all_registered_ops())
|
|
print(f"flag_gems {flag_gems.__version__}, {len(registered)} ops registered\n")
|
|
|
|
def served(op: str) -> bool:
|
|
"""Did FlagGems actually take this op?
|
|
|
|
Without this the screen lies by omission: `torch.bitwise_xor` has no FlagGems registration at
|
|
all, so a "match" there is PyTorch compared with PyTorch and says nothing about FlagGems. Any
|
|
case that is not served is reported as such and cannot enter an allow-list.
|
|
"""
|
|
return any(n == op or n.startswith(f"{op}.") or n.startswith(f"{op}_") for n in registered)
|
|
|
|
bad: dict[str, list[str]] = {}
|
|
unserved: set[str] = set()
|
|
rows = []
|
|
for c in cases:
|
|
if not served(c.op):
|
|
unserved.add(c.op)
|
|
rows.append((c.op, c.label, None, "not registered in flag_gems (PyTorch answered)"))
|
|
continue
|
|
try:
|
|
got = c.run()
|
|
torch.cuda.synchronize()
|
|
ok, msg = same(c.ref, got)
|
|
except Exception as exc: # noqa: BLE001 - an exception is itself a finding
|
|
ok, msg = False, f"raised {type(exc).__name__}: {str(exc)[:70]}"
|
|
rows.append((c.op, c.label, ok, msg))
|
|
if not ok:
|
|
bad.setdefault(c.op, []).append(c.label)
|
|
|
|
width = max(len(r[0]) for r in rows) + 2
|
|
for op, label, ok, msg in rows:
|
|
mark = "skip" if ok is None else ("ok " if ok else "BAD ")
|
|
print(f" {mark} {op:<{width}} {label:<16} {msg}")
|
|
|
|
if unserved:
|
|
print(f"\nnot served by flag_gems (cannot be judged here): {sorted(unserved)}")
|
|
print()
|
|
if bad:
|
|
print("UNSAFE OPS (exclude these):")
|
|
for op, labels in sorted(bad.items()):
|
|
print(f" {op}: {', '.join(labels)}")
|
|
return 1
|
|
print("all served ops matched")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|