mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-11-09 22:59:44 +08:00
feat(h20): enable FlagGems end to end, from an allow-list of ops proven equal to PyTorch
`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.
This commit is contained in:
parent
32d3d2c6bf
commit
dcf00aca7c
@ -23,7 +23,7 @@ main (8b16149)
|
|||||||
| `h20-sm90-triton` | `73b8090` | 引擎侧 29 个 kernel 加 Triton 实现(17 个可用),`DEEPINFER_OPS_BACKEND=triton` 切换 | 融合算子库 `tile_kernels` 仍要 TileLang |
|
| `h20-sm90-triton` | `73b8090` | 引擎侧 29 个 kernel 加 Triton 实现(17 个可用),`DEEPINFER_OPS_BACKEND=triton` 切换 | 融合算子库 `tile_kernels` 仍要 TileLang |
|
||||||
| `h20-sm90-triton-flagtree` | `c6a65fe` | 在 flagtree 镜像(torch 2.11 + cu13)里重编 `deepinfer._C` / `deep_gemm` / `deep_ep` / `deep_select`;`tile_kernels` 加 compat 层走 torch 参考,从而不 import TileLang;修掉参考实现在 sync 守卫下抛错的问题 | attention 仍要 TileLang |
|
| `h20-sm90-triton-flagtree` | `c6a65fe` | 在 flagtree 镜像(torch 2.11 + cu13)里重编 `deepinfer._C` / `deep_gemm` / `deep_ep` / `deep_select`;`tile_kernels` 加 compat 层走 torch 参考,从而不 import TileLang;修掉参考实现在 sync 守卫下抛错的问题 | attention 仍要 TileLang |
|
||||||
| `h20-sm90-sparse-mla-triton` | `6d337a8` | **attention 也换成 FlagGems 的 Triton kernel,于是「屏蔽 tilelang 也能端到端」成立** | 采样器的 rejection-sampling 变体仍要 TileLang(关 MTP 可绕);不与 TileLang 版逐 token 一致 |
|
| `h20-sm90-sparse-mla-triton` | `6d337a8` | **attention 也换成 FlagGems 的 Triton kernel,于是「屏蔽 tilelang 也能端到端」成立** | 采样器的 rejection-sampling 变体仍要 TileLang(关 MTP 可绕);不与 TileLang 版逐 token 一致 |
|
||||||
| **`h20-sm90-flaggems`** | **进行中** | **把 `tile_kernels` 的 torch 参考换成 FlagGems 的 Triton 实现**(先做已验证的 per-token quant) | 刚起步;`mhc_*` / `engram_*` 多数没有 1:1 对应 |
|
| **`h20-sm90-flaggems`** | **进行中** | FlagGems 白名单启用已端到端跑通(19 算子、8/8 DP、0 回退);per-token quant 已验证逐位相同 | 只覆盖点状/reduction 算子;GEMM 留 cuBLAS;token 级与不用时不同但确定性 |
|
||||||
|
|
||||||
## `h20-sm90-sparse-mla-triton` 到底做了什么
|
## `h20-sm90-sparse-mla-triton` 到底做了什么
|
||||||
|
|
||||||
|
|||||||
@ -78,6 +78,66 @@ use_tma_aligned_col_major_sf=True)`,5 个形状:
|
|||||||
|
|
||||||
`tilelang_ops/` 已经有 Triton 后端,`DEEPINFER_OPS_BACKEND=triton` 切换,与 flag_gems 无关。
|
`tilelang_ops/` 已经有 Triton 后端,`DEEPINFER_OPS_BACKEND=triton` 切换,与 flag_gems 无关。
|
||||||
|
|
||||||
|
## 端到端已跑通(2026-10-26):白名单模式
|
||||||
|
|
||||||
|
`DEEPINFER_ENABLE_FLAGGEMS=1` → `deepinfer/flaggems_bootstrap.py` 在 `create_llm` 里、
|
||||||
|
`setup_cuda_visible_devices()` **之后**调 `flag_gems.only_enable(include=[...])`。
|
||||||
|
|
||||||
|
结果:
|
||||||
|
|
||||||
|
| | 值 |
|
||||||
|
|---|---|
|
||||||
|
| exit code | **0** |
|
||||||
|
| 后端 | sparse MLA = triton,FlagGems mode=allow |
|
||||||
|
| DP 一致 | **8/8** |
|
||||||
|
| kernel 回退 | **无** |
|
||||||
|
| hard failures | **none** |
|
||||||
|
| 回答 | 「中国的首都→北京」「日本的首都→东京」,带分段格式 |
|
||||||
|
| 实际派发算子 | **19 个**、152 处调用点 |
|
||||||
|
| 注册 | 37 个请求名 → 96 个变体 → 43 个实际注册 |
|
||||||
|
| 警告 | 0 |
|
||||||
|
|
||||||
|
实际在跑的是 `abs/amax/bincount/clamp(3变体)/cos/exp/gather/index_select/neg/nonzero/
|
||||||
|
reciprocal/relu/rsqrt/sigmoid/sin/sqrt/stack` 这些——点状与 reduction 算子,正是 eager
|
||||||
|
PyTorch 一次一个 launch 吃亏、Triton 能合并的地方。
|
||||||
|
|
||||||
|
### 为什么改成白名单,而不是「全开 + 排除坏的」
|
||||||
|
|
||||||
|
我先按「全开+排除」做了 5 轮,每轮都在完整引擎里撞出一个新问题,每轮约 5 分钟:
|
||||||
|
`cat`(e8m0 dtype) → `mul`(广播形状) → `topk`(编译爆炸) → `rms_norm`(签名) → `mm`(编译断言)。
|
||||||
|
这条路的性质是「必须先被坑到才能修」,且中途任何状态都是不安全的。
|
||||||
|
|
||||||
|
白名单相反:**任何中间状态都是安全的**(没验证的算子留在 PyTorch),不完整只是性能问题,
|
||||||
|
不是正确性问题。这正好对应「能用就用、不能用就拉到」。
|
||||||
|
|
||||||
|
### 踩到的两个方法论错误(都已修)
|
||||||
|
|
||||||
|
1. **`torch.mul` 被替换后不能再当参考**。第一版 `ab_mul_dtype.py` 在 `enable()` 之后同时用了
|
||||||
|
`torch.mul` 和 `flag_gems.mul`,于是「bf16 mul 0 差异」是自比自。现在所有参考值都在
|
||||||
|
`enable()` **之前**算好并 clone。
|
||||||
|
2. **「ok」可能根本没走 FlagGems**。`torch.bitwise_xor` 在 FlagGems 里**没有实现**,所以
|
||||||
|
「bitwise_xor 通过」是 PyTorch 比 PyTorch。`screen_ops.py` 现在先判断该算子是否真的被注册,
|
||||||
|
否则标 `skip` 并说明「PyTorch answered」。`square` 同理(FlagGems 用别的名字)。
|
||||||
|
|
||||||
|
### 名字对不上是坑
|
||||||
|
|
||||||
|
FlagGems 的注册名是混合风格:`add`/`mul` 是裸名,但 `eq.Scalar`、`lt.Tensor`、`where.self`、
|
||||||
|
`repeat_interleave.self_int`、`_softmax`(不是 `softmax`)、`div.true_divide`、`pow.pow_scalar`
|
||||||
|
是 aten 风格的。只写 `softmax` 会**静默什么都不注册**。现在 `_expand()` 对着
|
||||||
|
`flag_gems._FULL_CONFIG` 自己做前缀展开,并在匹配不到时打印警告(这次就是靠它发现 `softmax`
|
||||||
|
应写 `_softmax`)。
|
||||||
|
|
||||||
|
### 仍未解决
|
||||||
|
|
||||||
|
- **`mm` 家族**:隔离探测(M/N/K 扫描、NT 布局、batch、addmm)全部正确,但引擎里仍在
|
||||||
|
`general_mm` 撞 `accumulator shape is incompatible`。触发条件没定位,因此 GEMM 继续走 cuBLAS
|
||||||
|
(本来也更快)。
|
||||||
|
- **token 级不一致,但是确定性的**:与不用 FlagGems 的版本对比,第 1 个 token 就分叉(上一版
|
||||||
|
sparse-MLA-only 是前 15 个一致)。但**同配置连跑两次逐 token 完全相同**,所以差异来自
|
||||||
|
Triton kernel 与 eager PyTorch 的累加顺序,不是不确定性。回答仍然正确连贯,与 SM90 已知
|
||||||
|
2.7% logits 漂移同源,被贪心解码放大。
|
||||||
|
- **性能完全没测**。白名单里只有 19 个算子真被用到,收益未知。
|
||||||
|
|
||||||
## 建议的推进顺序
|
## 建议的推进顺序
|
||||||
|
|
||||||
每一步都是「先 A/B 单测、通过才接线」,沿用 sparse MLA 那次的规矩。
|
每一步都是「先 A/B 单测、通过才接线」,沿用 sparse MLA 那次的规矩。
|
||||||
|
|||||||
227
deepinfer/flaggems_bootstrap.py
Normal file
227
deepinfer/flaggems_bootstrap.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
"""Optionally route PyTorch operators through FlagGems, via its documented `enable()` / `only_enable()`.
|
||||||
|
|
||||||
|
FlagGems' usage docs (`docs/content/en/usage/basic.md`, `.../selective.md`) prescribe:
|
||||||
|
|
||||||
|
import flag_gems
|
||||||
|
flag_gems.enable() # all supported ops
|
||||||
|
flag_gems.only_enable(include=[...]) # only the named ops
|
||||||
|
|
||||||
|
This module defaults to the **allow-list** form, because flag_gems 5.0.2 on SM90 has operators that are
|
||||||
|
outright wrong, and a silently wrong operator is worse than a slow correct one. Only ops proven equal
|
||||||
|
to PyTorch on this build are enabled; everything else stays on PyTorch, which is correct by
|
||||||
|
construction. Measured by `scripts/flaggems_port/screen_ops.py`; extend `_VERIFIED_OPS` only after that
|
||||||
|
screen (or an equivalent A/B) passes.
|
||||||
|
|
||||||
|
Why an allow-list rather than "enable everything then exclude what breaks": a deny-list can only grow
|
||||||
|
after each failure is hit inside a full engine run, and each cycle surfaces one operator. The
|
||||||
|
allow-list is safe at every intermediate state, so an incomplete list is a performance question rather
|
||||||
|
than a correctness one.
|
||||||
|
|
||||||
|
Env:
|
||||||
|
|
||||||
|
`DEEPINFER_ENABLE_FLAGGEMS=1` turn it on
|
||||||
|
`DEEPINFER_FLAGGEMS_MODE=allow|deny` default `allow` (curated) vs `deny` (all but the broken ones)
|
||||||
|
`DEEPINFER_FLAGGEMS_INCLUDE=a,b` extra ops for allow mode
|
||||||
|
`DEEPINFER_FLAGGEMS_EXCLUDE=a,b` extra ops to keep on PyTorch, in either mode
|
||||||
|
`DEEPINFER_FLAGGEMS_RECORD_DIR=<dir>` write `<dir>/oplist-<pid>.log`, one line per call site
|
||||||
|
|
||||||
|
**Where it is called matters more than what it does.** `import flag_gems` initialises a CUDA context
|
||||||
|
(measured: `torch.cuda.is_initialized()` flips False -> True on the import alone, before `enable()`).
|
||||||
|
If that happens before this process is pinned to its GPU, every rank initialises on device 0 and the
|
||||||
|
engine fails at the first collective with
|
||||||
|
|
||||||
|
NCCL error: invalid usage
|
||||||
|
Last error: Multiple Ranks are using the same GPU/Partition.
|
||||||
|
|
||||||
|
That is what a `sitecustomize` injector produced, because `sitecustomize` runs at interpreter startup
|
||||||
|
while the per-rank `CUDA_VISIBLE_DEVICES` is set later, inside `create_llm`. The same hazard is already
|
||||||
|
documented there for the profiler ("CUPTI 启动即初始化 CUDA,会使此后设置的 CUDA_VISIBLE_DEVICES 失效").
|
||||||
|
Hence this is called from `create_llm` immediately after `setup_cuda_visible_devices`, and there is no
|
||||||
|
`sitecustomize` for it.
|
||||||
|
|
||||||
|
**Measured on flag_gems 5.0.2 / H20 (SM90), which is why the lists below look the way they do:**
|
||||||
|
|
||||||
|
* `mul` returns the **first operand's shape** when broadcasting would expand it -- `(1,64,8)*(4,1,8)`
|
||||||
|
gives `(1,64,8)` instead of `(4,64,8)`; `(64,1)*(1,128)` gives `(64,1)`. Silent, no error, fewer
|
||||||
|
elements computed. Same-shape and scalar `mul` are exact. `add`/`sub`/`div`/`maximum`/`minimum` all
|
||||||
|
broadcast correctly, so it is `mul`'s own bug. It produced a wrong result inside `engram_hash_ref`
|
||||||
|
during a real engine run.
|
||||||
|
* `cumsum` disagrees with PyTorch by up to 1.0 over a 512-element bf16 row where the tolerance is
|
||||||
|
~0.17 -- bf16 accumulation where PyTorch uses fp32.
|
||||||
|
* `rms_norm` requires `weight` positionally where `torch.nn.functional.rms_norm` allows omitting it,
|
||||||
|
and with `weight` supplied still differs by ~3e-2 on O(1) output.
|
||||||
|
* `topk`'s compile time is superlinear in `k` and the sampler uses `k=4096`: k=8 -> 4.8 s, k=64 -> 19 s,
|
||||||
|
k=512 -> >180 s. The startup profile run never returned; `py-spy` put all 8 ranks at 100% CPU inside
|
||||||
|
`triton/backends/nvidia/compiler.py:make_llir` with every GPU idle.
|
||||||
|
* `cat` raises `KeyError: 'float8_e8m0fnu'` from Triton's dtype table on the UE8M0 scale tensors this
|
||||||
|
model stores, during weight loading. Triton has no e8m0 element type.
|
||||||
|
* the `mm` family passes every isolated probe (M/N/K swept, NT layout, batch, addmm) yet the engine
|
||||||
|
still hit `accumulator shape is incompatible` inside `general_mm`; trigger not identified, so GEMMs
|
||||||
|
stay on cuBLAS -- which is also the faster choice.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
_ENV_ENABLE = "DEEPINFER_ENABLE_FLAGGEMS"
|
||||||
|
_ENV_MODE = "DEEPINFER_FLAGGEMS_MODE"
|
||||||
|
_ENV_INCLUDE = "DEEPINFER_FLAGGEMS_INCLUDE"
|
||||||
|
_ENV_EXCLUDE = "DEEPINFER_FLAGGEMS_EXCLUDE"
|
||||||
|
_ENV_RECORD_DIR = "DEEPINFER_FLAGGEMS_RECORD_DIR"
|
||||||
|
|
||||||
|
# Ops measured equal to PyTorch by `scripts/flaggems_port/screen_ops.py`, across the shape variants
|
||||||
|
# that screen covers (same-shape, broadcast, scalar, unary, reduction). Pointwise and reduction ops
|
||||||
|
# are where FlagGems can actually win: one kernel launch instead of several eager calls. GEMMs are
|
||||||
|
# deliberately absent -- cuBLAS wins there regardless.
|
||||||
|
_VERIFIED_OPS: tuple[str, ...] = (
|
||||||
|
"abs",
|
||||||
|
"add",
|
||||||
|
"amax",
|
||||||
|
"bincount",
|
||||||
|
"bitwise_and",
|
||||||
|
"clamp",
|
||||||
|
"cos",
|
||||||
|
"div",
|
||||||
|
"eq",
|
||||||
|
"exp",
|
||||||
|
"gather",
|
||||||
|
"ge",
|
||||||
|
"gelu",
|
||||||
|
"index_select",
|
||||||
|
"lt",
|
||||||
|
"masked_fill",
|
||||||
|
"maximum",
|
||||||
|
"mean",
|
||||||
|
"minimum",
|
||||||
|
"neg",
|
||||||
|
"nonzero",
|
||||||
|
"pow",
|
||||||
|
"reciprocal",
|
||||||
|
"relu",
|
||||||
|
"remainder",
|
||||||
|
"repeat_interleave",
|
||||||
|
"rsqrt",
|
||||||
|
"sigmoid",
|
||||||
|
"silu",
|
||||||
|
"sin",
|
||||||
|
"_softmax",
|
||||||
|
"sort",
|
||||||
|
"sqrt",
|
||||||
|
"stack",
|
||||||
|
"sub",
|
||||||
|
"sum",
|
||||||
|
"where",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Everything known broken on this build, documented and used by deny mode. Nothing here may move into
|
||||||
|
# `_VERIFIED_OPS` without a passing A/B.
|
||||||
|
_BROKEN_OPS: tuple[str, ...] = ("cat", "cumsum", "mul", "rms_norm", "topk")
|
||||||
|
|
||||||
|
_enabled = False
|
||||||
|
|
||||||
|
|
||||||
|
def enabled() -> bool:
|
||||||
|
"""Whether FlagGems has been enabled in this process."""
|
||||||
|
return _enabled
|
||||||
|
|
||||||
|
|
||||||
|
def _names(raw: str) -> list[str]:
|
||||||
|
return [op.strip() for op in raw.split(",") if op.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def _expand(names: list[str], available: list[str]) -> tuple[list[str], list[str]]:
|
||||||
|
"""Resolve plain op names to the names FlagGems actually registers.
|
||||||
|
|
||||||
|
FlagGems registers a mix of plain names (`add`, `mul`) and aten-style dotted or suffixed names:
|
||||||
|
`eq.Scalar`, `lt.Tensor`, `where.self`, `repeat_interleave.self_int`, `_softmax`, `div.true_divide`,
|
||||||
|
`pow.pow_scalar`. Asking for `eq` by name therefore registers nothing, silently -- caught here as a
|
||||||
|
warning, but the fix is to expand to every variant rather than spell out each one in
|
||||||
|
`_VERIFIED_OPS`.
|
||||||
|
|
||||||
|
Returns (expanded, unresolved). `available` is FlagGems' own registration table, so this cannot
|
||||||
|
drift from what the installed version actually offers.
|
||||||
|
"""
|
||||||
|
expanded: list[str] = []
|
||||||
|
unresolved: list[str] = []
|
||||||
|
for name in names:
|
||||||
|
hits = [
|
||||||
|
n
|
||||||
|
for n in available
|
||||||
|
if n == name or n.startswith(f"{name}_") or n.startswith(f"{name}.")
|
||||||
|
]
|
||||||
|
if hits:
|
||||||
|
expanded.extend(hits)
|
||||||
|
else:
|
||||||
|
unresolved.append(name)
|
||||||
|
# dict.fromkeys: preserve order, drop duplicates (a name may match several wanted entries).
|
||||||
|
return list(dict.fromkeys(expanded)), unresolved
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_enable() -> str:
|
||||||
|
"""Enable FlagGems if asked to. Returns a one-line status; no-op when the env var is unset."""
|
||||||
|
global _enabled
|
||||||
|
|
||||||
|
if _enabled:
|
||||||
|
return "already enabled"
|
||||||
|
if not os.environ.get(_ENV_ENABLE):
|
||||||
|
return "disabled"
|
||||||
|
|
||||||
|
try:
|
||||||
|
import flag_gems
|
||||||
|
except Exception as exc: # noqa: BLE001 - a requested-but-broken import must be loud
|
||||||
|
raise RuntimeError(
|
||||||
|
f"{_ENV_ENABLE} is set but flag_gems is not importable: {type(exc).__name__}: {exc}. "
|
||||||
|
f"Unset it, or fix the install (it also needs libcudart on LD_LIBRARY_PATH for Triton TLE)."
|
||||||
|
) from exc
|
||||||
|
|
||||||
|
mode = (os.environ.get(_ENV_MODE, "allow").strip().lower() or "allow")
|
||||||
|
extra_in = _names(os.environ.get(_ENV_INCLUDE, ""))
|
||||||
|
extra_out = _names(os.environ.get(_ENV_EXCLUDE, ""))
|
||||||
|
|
||||||
|
log_kwargs: dict[str, object] = {}
|
||||||
|
record_dir = os.environ.get(_ENV_RECORD_DIR, "").strip()
|
||||||
|
if record_dir:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
Path(record_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
# Per-PID: `setup_flaggems_logging` opens the path with mode="w", so ranks sharing one path
|
||||||
|
# would leave only whichever wrote last.
|
||||||
|
log_kwargs = {
|
||||||
|
"record": True,
|
||||||
|
"path": str(Path(record_dir) / f"oplist-{os.getpid()}.log"),
|
||||||
|
"once": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
include: list[str] | None = None
|
||||||
|
if mode == "allow":
|
||||||
|
excluded = set(extra_out)
|
||||||
|
wanted = [op for op in (*_VERIFIED_OPS, *extra_in) if op not in excluded]
|
||||||
|
# Expand against FlagGems' own table, not a hand-maintained list of alias names. Entries are
|
||||||
|
# variable-length tuples whose first field is the op name.
|
||||||
|
available = [entry[0] for entry in flag_gems._FULL_CONFIG] # noqa: SLF001
|
||||||
|
include, unresolved = _expand(wanted, available)
|
||||||
|
flag_gems.only_enable(include=include, **log_kwargs)
|
||||||
|
how = f"only_enable({len(wanted)} wanted -> {len(include)} registered)"
|
||||||
|
elif mode == "deny":
|
||||||
|
unused = [op for op in (*_BROKEN_OPS, *extra_out)]
|
||||||
|
unresolved = []
|
||||||
|
flag_gems.enable(unused=unused, **log_kwargs)
|
||||||
|
how = f"enable(unused={unused})"
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"{_ENV_MODE}={mode!r} is not recognised; use 'allow' or 'deny'")
|
||||||
|
|
||||||
|
registered = set(flag_gems.all_registered_ops())
|
||||||
|
if unresolved:
|
||||||
|
# Nothing would be enabled for these, which reads as an unexplained slowdown rather than a
|
||||||
|
# failure. Say it once, loudly.
|
||||||
|
print(f"[flaggems] WARNING: no registered op matches: {unresolved}", flush=True)
|
||||||
|
|
||||||
|
_enabled = True
|
||||||
|
status = (
|
||||||
|
f"enabled pid={os.getpid()} flag_gems={flag_gems.__version__} "
|
||||||
|
f"device={flag_gems.device}/{flag_gems.vendor_name} mode={mode} {how} "
|
||||||
|
f"registered={len(registered)}"
|
||||||
|
)
|
||||||
|
print(f"[flaggems] {status}", flush=True)
|
||||||
|
return status
|
||||||
@ -107,6 +107,13 @@ def create_llm(
|
|||||||
|
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
|
# After the device pin, not before: importing flag_gems initialises a CUDA context, so doing it
|
||||||
|
# earlier puts every rank on device 0 and NCCL then refuses with "Multiple Ranks are using the
|
||||||
|
# same GPU". No-op unless DEEPINFER_ENABLE_FLAGGEMS is set.
|
||||||
|
from deepinfer.flaggems_bootstrap import maybe_enable as maybe_enable_flaggems
|
||||||
|
|
||||||
|
maybe_enable_flaggems()
|
||||||
|
|
||||||
from deepinfer.entrypoints.llm import LLM
|
from deepinfer.entrypoints.llm import LLM
|
||||||
|
|
||||||
with torch.profiler.profile() if profile_init else nullcontext():
|
with torch.profiler.profile() if profile_init else nullcontext():
|
||||||
|
|||||||
111
scripts/flaggems_port/ab_mul_dtype.py
Normal file
111
scripts/flaggems_port/ab_mul_dtype.py
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
"""Is FlagGems' `mul` correct, and for which dtypes?
|
||||||
|
|
||||||
|
Method note, because the first version of this script was wrong: once `flag_gems.enable()` runs it also
|
||||||
|
captures `torch.mul`, so comparing `torch.mul(...)` with `flag_gems.mul(...)` compares FlagGems with
|
||||||
|
itself. Every reference here is therefore computed **before** `enable()`, in the same process.
|
||||||
|
|
||||||
|
The defect it measures: `engram_hash_ref` does `int64 (1,T,N) * int64 (L,1,N)`, and FlagGems returns a
|
||||||
|
tensor of the *first operand's* shape rather than the broadcast shape, so most of the result is never
|
||||||
|
written. Excluding `mul` from `enable()` restores the reference result exactly, which is what pointed
|
||||||
|
here in the first place.
|
||||||
|
|
||||||
|
Run: PYTHONPATH=. python scripts/flaggems_port/ab_mul_dtype.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
FAILED: list[str] = []
|
||||||
|
|
||||||
|
|
||||||
|
def check(tag: str, cond: bool, detail: str = "") -> None:
|
||||||
|
print(f" {'ok ' if cond else 'BAD '} {tag}{' ' + detail if detail else ''}")
|
||||||
|
if not cond:
|
||||||
|
FAILED.append(tag)
|
||||||
|
|
||||||
|
|
||||||
|
def compare(tag: str, ref: torch.Tensor, got: torch.Tensor) -> None:
|
||||||
|
if ref.shape != got.shape:
|
||||||
|
check(tag, False, f"shape ref {tuple(ref.shape)} vs got {tuple(got.shape)}")
|
||||||
|
return
|
||||||
|
if ref.dtype != got.dtype:
|
||||||
|
check(tag, False, f"dtype ref {ref.dtype} vs got {got.dtype}")
|
||||||
|
return
|
||||||
|
if ref.dtype.is_floating_point:
|
||||||
|
diff = (ref.float() - got.float()).abs().max().item()
|
||||||
|
scale = ref.float().abs().max().item()
|
||||||
|
tol = 2.0**-8 * max(scale, 1e-9) * 2
|
||||||
|
check(tag, diff <= tol, f"max diff {diff:.3e} tol {tol:.3e}")
|
||||||
|
return
|
||||||
|
same = torch.equal(ref, got)
|
||||||
|
detail = ""
|
||||||
|
if not same:
|
||||||
|
bad = (ref != got).sum().item()
|
||||||
|
idx = (ref != got).nonzero()[:3]
|
||||||
|
sample = [(ref[tuple(i)].item(), got[tuple(i)].item()) for i in idx]
|
||||||
|
detail = f"{bad}/{ref.numel()} wrong, (ref, got) {sample}"
|
||||||
|
check(tag, same, detail)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
torch.manual_seed(0)
|
||||||
|
dev = "cuda"
|
||||||
|
|
||||||
|
# ---- references, computed while torch.mul is still PyTorch's ----
|
||||||
|
a = torch.randint(0, 1000, (1, 64, 8), device=dev, dtype=torch.int64)
|
||||||
|
b = torch.randint(1, 100, (4, 1, 8), device=dev, dtype=torch.int64)
|
||||||
|
ref_bcast = torch.mul(a, b).clone()
|
||||||
|
|
||||||
|
a_l = torch.randint(2**40, 2**41, (256,), device=dev, dtype=torch.int64)
|
||||||
|
ref_large = torch.mul(a_l, a_l).clone()
|
||||||
|
|
||||||
|
a_i32 = torch.randint(0, 4096, (1024,), device=dev, dtype=torch.int32)
|
||||||
|
ref_i32 = torch.mul(a_i32, a_i32).clone()
|
||||||
|
|
||||||
|
a_bf = torch.randn((2048, 4096), device=dev, dtype=torch.bfloat16) * 0.1
|
||||||
|
b_bf = torch.randn((2048, 4096), device=dev, dtype=torch.bfloat16) * 0.1
|
||||||
|
ref_bf16 = torch.mul(a_bf, b_bf).clone()
|
||||||
|
|
||||||
|
a_bf_s = torch.randn((4096,), device=dev, dtype=torch.bfloat16)
|
||||||
|
ref_bf16_scalar = torch.mul(a_bf_s, 3).clone()
|
||||||
|
ref_i64_scalar = torch.mul(a, 3).clone()
|
||||||
|
|
||||||
|
import flag_gems
|
||||||
|
|
||||||
|
flag_gems.enable()
|
||||||
|
print(f"flag_gems {flag_gems.__version__}: enable() done")
|
||||||
|
print(f"reference broadcast shape: {tuple(ref_bcast.shape)}\n")
|
||||||
|
|
||||||
|
cases: list[tuple[str, torch.Tensor, Callable[[], torch.Tensor]]] = [
|
||||||
|
("int64 mul, broadcasting (1,64,8)*(4,1,8)", ref_bcast, lambda: flag_gems.mul(a, b)),
|
||||||
|
("int64 mul, large values", ref_large, lambda: flag_gems.mul(a_l, a_l)),
|
||||||
|
("int32 mul", ref_i32, lambda: flag_gems.mul(a_i32, a_i32)),
|
||||||
|
("bf16 mul, 8M elements", ref_bf16, lambda: flag_gems.mul(a_bf, b_bf)),
|
||||||
|
("bf16 mul by scalar", ref_bf16_scalar, lambda: flag_gems.mul(a_bf_s, 3)),
|
||||||
|
("int64 mul by scalar", ref_i64_scalar, lambda: flag_gems.mul(a, 3)),
|
||||||
|
]
|
||||||
|
for tag, ref, fn in cases:
|
||||||
|
got = fn()
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
compare(tag, ref, got)
|
||||||
|
|
||||||
|
# torch.mul must show the same defect. If only the direct call did, the dispatcher would be
|
||||||
|
# fine and the fix would live elsewhere.
|
||||||
|
got_dispatch = torch.mul(a, b)
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
compare("torch.mul (dispatched) shows the same defect", ref_bcast, got_dispatch)
|
||||||
|
|
||||||
|
print()
|
||||||
|
if FAILED:
|
||||||
|
print(f"DEFECTS: {FAILED}")
|
||||||
|
return 1
|
||||||
|
print("no defect found in the dtypes tested")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
73
scripts/flaggems_port/repro_engram_hash.py
Normal file
73
scripts/flaggems_port/repro_engram_hash.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
"""Reproduce the `engram_hash_ref` failure seen with FlagGems enabled, and bisect it.
|
||||||
|
|
||||||
|
The end-to-end run failed inside `tile_kernels.torch.engram.engram_hash_ref` with
|
||||||
|
|
||||||
|
IndexError: index 1 is out of bounds for dimension 0 with size 1
|
||||||
|
|
||||||
|
That function is a plain PyTorch reference, so either FlagGems changed an operator's semantics, or
|
||||||
|
the engine's own call path is at fault and enabling FlagGems merely shifted when it surfaced. Those
|
||||||
|
have different fixes, so this script answers "is it FlagGems" first, in-process, without a full
|
||||||
|
engine run.
|
||||||
|
|
||||||
|
Run with FLAGGEMS_REPRO_MODE=torch|full|unused:
|
||||||
|
torch -- do not enable (control)
|
||||||
|
full -- flag_gems.enable()
|
||||||
|
unused -- flag_gems.enable(unused=[...]) with FLAGGEMS_REPRO_EXCLUDE=op1,op2
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from tile_kernels.torch.engram import engram_hash_ref
|
||||||
|
|
||||||
|
MODE = os.environ.get("FLAGGEMS_REPRO_MODE", "torch")
|
||||||
|
|
||||||
|
|
||||||
|
def enable() -> str:
|
||||||
|
if MODE == "torch":
|
||||||
|
return "flag_gems: off (control)"
|
||||||
|
import flag_gems
|
||||||
|
|
||||||
|
if MODE == "full":
|
||||||
|
flag_gems.enable()
|
||||||
|
return f"flag_gems: full enable, {len(flag_gems.all_registered_ops())} ops"
|
||||||
|
exclude = [op.strip() for op in os.environ.get("FLAGGEMS_REPRO_EXCLUDE", "").split(",") if op.strip()]
|
||||||
|
flag_gems.enable(unused=exclude)
|
||||||
|
return f"flag_gems: enable(unused={exclude})"
|
||||||
|
|
||||||
|
|
||||||
|
def build(device: str = "cuda") -> tuple[torch.Tensor, ...]:
|
||||||
|
"""Shapes from the engine's config rather than invented ones."""
|
||||||
|
num_ngram_layers = int(os.environ.get("REPRO_LAYERS", "1"))
|
||||||
|
max_ngram_size = int(os.environ.get("REPRO_NGRAM", "2"))
|
||||||
|
tables = 2
|
||||||
|
num_tokens = int(os.environ.get("REPRO_TOKENS", "16"))
|
||||||
|
vocab = 4096
|
||||||
|
|
||||||
|
torch.manual_seed(0)
|
||||||
|
# (num_tokens, max_ngram_size) per the docstring: unsqueeze(0) then broadcast against
|
||||||
|
# multipliers.unsqueeze(1) is what builds (layers, tokens, ngram).
|
||||||
|
ngram_token_ids = torch.randint(0, 1000, (num_tokens, max_ngram_size), device=device, dtype=torch.int32)
|
||||||
|
multipliers = torch.randint(1, 100, (num_ngram_layers, max_ngram_size), device=device, dtype=torch.int64)
|
||||||
|
vocab_sizes = torch.full((num_ngram_layers, max_ngram_size - 1, tables), vocab, device=device, dtype=torch.int32)
|
||||||
|
offsets = torch.zeros((num_ngram_layers, (max_ngram_size - 1) * tables), device=device, dtype=torch.int32)
|
||||||
|
return ngram_token_ids, multipliers, vocab_sizes, offsets
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(enable(), flush=True)
|
||||||
|
args = build()
|
||||||
|
try:
|
||||||
|
out = engram_hash_ref(*args)
|
||||||
|
torch.cuda.synchronize()
|
||||||
|
print(f"OK out {tuple(out.shape)} {out.dtype} finite={bool(torch.isfinite(out.float()).all())}")
|
||||||
|
# Print a checksum so runs with different exclude lists can be compared.
|
||||||
|
print(f" checksum {int(out.to(torch.int64).abs().sum())}")
|
||||||
|
except Exception:
|
||||||
|
print("FAIL")
|
||||||
|
traceback.print_exc()
|
||||||
|
sys.exit(1)
|
||||||
228
scripts/flaggems_port/screen_ops.py
Normal file
228
scripts/flaggems_port/screen_ops.py
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
"""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())
|
||||||
Loading…
x
Reference in New Issue
Block a user