mirror of
https://github.com/deepseek-ai/DeepInfer.git
synced 2026-10-31 00:36:20 +08:00
feat(h20): FlagGems' Triton sparse MLA matches the TileLang kernel at V4.1's shape
The remaining blocker for a TileLang-free path was sparse MLA: `h20_ops/sparse_mla_sm90.py` is a
231-line TileLang program with no Triton counterpart in this repo. FlagGems already has one, and it
turns out to be drop-in.
`flagos-ai/FlagGems` -> `src/flag_gems/fused/flashmla_sparse.py` -> `flash_mla_sparse_fwd`. Pure
Triton. At V4.1's real dims (h_q=64, h_kv=1, d_qk=512, topk=512) it compiles and runs on H20, and
on 7 shapes it agrees with the TileLang kernel within 2 bf16 ULP -- including `topk_length` and
`attn_sink`, the two optional arguments.
The pairing is closer than expected, which is why it needed no adaptation:
prefill_attn_varlen_sparse(query, kv, index_topk, softmax_scale, attn_sink, index_length)
flash_mla_sparse_fwd(q, kv, indices, sm_scale, attn_sink, topk_length)
Causality matches too, and for the same reason. `attn_sm90.py` passes `is_causal=False` because the
indexer only scores positions before each query, so the indices already carry causality -- bounding
on the row index instead collapses the layer to NaN, which already happened once and is documented
at that call site. The FlagGems kernel has no causal parameter either.
Recorded as *not* the answer: `DSA/sparse_mla.py` in the same repo. It hardcodes causal and needs
the V3.2 576-wide split layout; V4.1 folds RoPE inside a single 512-wide latent, so TD = 0 and it
fails to compile on `tl.arange(0, 0)`. Worth writing down because the name looks like the better
match.
What remains, all much smaller than writing a kernel: wire it in behind the existing
`patch_imports` hook; test the decode path separately (it gathers to bf16 with identity indices, a
different call shape than the 7 cases); and convert the return, since the kernel gives
(out, max_logits, lse) with lse in natural log where DeepInfer expects (out, lse) with lse in log2
-- a 1.4427x scale error if dropped.
This commit is contained in:
parent
88567ff648
commit
c6a65fe6ad
42
TODO.md
42
TODO.md
@ -630,13 +630,45 @@ quant/MoE/MHC/engram,25 处模块级 import / 23 个 API)。
|
||||
4. prompt B 第 1 个 token 就分叉,暗示 prefill 阶段已存在实质差异,需要对比首个 token 的 logits
|
||||
才能定位,而不是继续猜。
|
||||
|
||||
### 6.6 还差什么才能真正脱离 tilelang
|
||||
### 6.6 稀疏 MLA 的 Triton 方案已找到,且已数值验证(2026-10-25)
|
||||
|
||||
原判断「SM90 稀疏 MLA 没有 Triton 版本、工作量远大于 compat 层」**需要修正**:
|
||||
FlagGems 里已经有,而且是 drop-in 的。
|
||||
|
||||
**`flagos-ai/FlagGems` → `src/flag_gems/fused/flashmla_sparse.py` → `flash_mla_sparse_fwd`**
|
||||
纯 Triton,实测在 H20 上编译执行通过,**7 组形状与现有 TileLang kernel 全部一致(≤2 bf16 ULP)**。
|
||||
|
||||
参数逐项对应,连两个可选参数都有:
|
||||
|
||||
| DeepInfer `prefill_attn_varlen_sparse` | FlagGems `flash_mla_sparse_fwd` |
|
||||
|---|---|
|
||||
| `query [total_q, H, D]` | `q [s_q, h_q, d]` |
|
||||
| `kv_a_with_mqa [total_k, 1, D]` | `kv [s_kv, h_kv, d_qk]` |
|
||||
| `index_topk [total_q, topk]` | `indices [s_q, h_kv, topk]` |
|
||||
| `softmax_scale` | `sm_scale` |
|
||||
| `attn_sink [H]` | `attn_sink [h_q]` |
|
||||
| `index_length [total_q]` | `topk_length [s_q]` |
|
||||
|
||||
**语义也对得上**:`attn_sm90.py:110` 明确让 prefill 走 `is_causal=False`(索引由 indexer 保证
|
||||
causal,用行号当因果边界会导致整层 NaN,这个 bug 已经发生过一次);FlagGems 这个 kernel 同样
|
||||
没有 causal 参数,理由一致。V4.1 的 `h_q=64 / h_kv=1 / d_qk=512` 恰好满足它所有 assert。
|
||||
|
||||
**不能用的那个**:同仓库 `DSA/sparse_mla.py`(`triton_sparse_mla_fwd_interface`)硬编码
|
||||
`is_causal=True`,且要求 V3.2 的 576 拆分布局(latent 512 + rope tail 64)。V4.1 把 RoPE 并进
|
||||
512 维内,`TD = 512-512 = 0`,`tl.arange(0,0)` 直接编译失败。别选错。
|
||||
|
||||
验证脚本与 vendor 副本:`scripts/sparse_mla_port/`(`README.md` 有完整对照表与未做项)。
|
||||
|
||||
**还差的三步**(都比重写 kernel 小得多):
|
||||
1. **接线**:`sparse_mla_sm90.py` 仍 `import tilelang`。用现成的 `patch_imports` 挂一个 Triton
|
||||
后端即可,不动上游调用点。
|
||||
2. **decode 路径单独测**:今天 decode 是 gather→bf16 + identity indices,与上面 7 组形状不同,
|
||||
需要对着 fp8 cache 路径另做一次 A/B。
|
||||
3. **返回值适配**:它返回 `(out, max_logits, lse)`,DeepInfer 要 `(out, lse)`;且 TileLang 的
|
||||
`lse` 是 log2 单位、FlagGems 是自然对数,**必须转换**,否则下游读到的 scale 差 1.4427 倍。
|
||||
|
||||
`h20_ops/sparse_mla_sm90.py` 是 `@tilelang.jit` 实现,且 `h20-sm90-triton` 分支上也是如此
|
||||
(该分支只是没屏蔽 tilelang,不是真的无依赖)。**SM90 稀疏 MLA 没有 Triton 版本**,这是
|
||||
「国产卡完全不用 tilelang」的最后一个硬缺口,工作量远大于这次的 compat 层。
|
||||
`tilelang_ops/cuda/` 那 29 个模块已有 `tilelang_ops/triton/` 对应版本,靠
|
||||
`DEEPINFER_OPS_BACKEND=triton` 切换,不是缺口。
|
||||
`DEEPINFER_OPS_BACKEND=triton` 切换,本来就不是缺口。
|
||||
|
||||
### 6.4 端到端曾经为什么跑不起来(环境 + 排查记录)
|
||||
|
||||
|
||||
64
scripts/sparse_mla_port/README.md
Normal file
64
scripts/sparse_mla_port/README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# FlagGems sparse-MLA as the SM90 attention backend
|
||||
|
||||
The SM90 sparse MLA kernel (`deepinfer/h20_ops/sparse_mla_sm90.py`) is TileLang-only, so with
|
||||
TileLang blocked the model cannot start. FlagGems ships a pure-Triton implementation of the same
|
||||
operator, and at DeepSeek-V4.1's exact shape it agrees with the TileLang kernel.
|
||||
|
||||
## Source
|
||||
|
||||
`flagos-ai/FlagGems`, file `src/flag_gems/fused/flashmla_sparse.py`, function
|
||||
`flash_mla_sparse_fwd`. Copied verbatim into `vendor/` for the tests; it depends only on
|
||||
`flag_gems.utils.triton_version_utils` (a Triton version probe), stubbed in `vendor/flag_gems/`.
|
||||
|
||||
Do not confuse it with `src/flag_gems/fused/DSA/sparse_mla.py`
|
||||
(`triton_sparse_mla_fwd_interface`). That one hardcodes `is_causal=True` and requires the V3.2
|
||||
576-wide split layout (latent 512 + rope tail 64). V4.1 folds RoPE inside a single 512-wide
|
||||
latent, so `TD = 512 - 512 = 0` and the kernel fails to compile on `tl.arange(0, 0)`. It is not a
|
||||
candidate.
|
||||
|
||||
## Why the pairing works
|
||||
|
||||
V4.1's dims satisfy every assert in the FlagGems kernel: `h_q = 64`, `h_kv = 1`, `d_qk = 512`.
|
||||
|
||||
The argument lists correspond item for item, including the two optional ones:
|
||||
|
||||
| DeepInfer `prefill_attn_varlen_sparse` | FlagGems `flash_mla_sparse_fwd` |
|
||||
|---|---|
|
||||
| `query [total_q, H, D]` | `q [s_q, h_q, d]` |
|
||||
| `kv_a_with_mqa [total_k, 1, D]` | `kv [s_kv, h_kv, d_qk]` |
|
||||
| `index_topk [total_q, topk]` | `indices [s_q, h_kv, topk]` |
|
||||
| `softmax_scale` | `sm_scale` |
|
||||
| `attn_sink [H]` | `attn_sink [h_q]` |
|
||||
| `index_length [total_q]` | `topk_length [s_q]` |
|
||||
|
||||
Causality also lines up: `attn_sm90.prefill_attn_varlen_sparse` passes `is_causal=False` and
|
||||
documents why -- the indexer only scores positions before each query, so the indices carry
|
||||
causality, and bounding on the row index instead collapses the layer to NaN (that already happened
|
||||
once, see the comment at `attn_sm90.py:110`). The FlagGems kernel has no causal parameter for the
|
||||
same reason.
|
||||
|
||||
## Result
|
||||
|
||||
`ab_vs_tilelang.py`, 7 shapes, TileLang as reference: **all match** within 2 bf16 ULP, including
|
||||
`topk_length` and `attn_sink`.
|
||||
|
||||
| case | max abs diff | tol |
|
||||
|---|---|---|
|
||||
| prefill, distinct top-k (SQ=64, SKV=2048) | 1.22e-04 | 1.45e-04 |
|
||||
| prefill, duplicate top-k | 1.22e-04 | 1.97e-04 |
|
||||
| decode (SQ=1, SKV=4096) | 6.10e-05 | 1.20e-04 |
|
||||
| odd SQ (7) | 6.10e-05 | 1.43e-04 |
|
||||
| short KV < topk | 2.44e-04 | 2.90e-04 |
|
||||
| with topk_length | 2.44e-04 | 3.20e-04 |
|
||||
| with attn_sink | 1.10e-04 | 1.41e-04 |
|
||||
|
||||
## What is not done yet
|
||||
|
||||
* Not wired in. `sparse_mla_sm90.py` still imports TileLang; a Triton backend behind the same
|
||||
`patch_imports` hook is the next step.
|
||||
* Decode path untested end to end. Today decode gathers to bf16 and calls the kernel with identity
|
||||
indices and no causal; that is a different call shape than the ones above and needs its own test
|
||||
against the fp8-cache path.
|
||||
* The kernel returns `(out, max_logits, lse)` while DeepInfer's returns `(out, lse)`, and TileLang's
|
||||
`lse` is in log2 units where FlagGems' is natural log. A wrapper must drop `max_logits` and
|
||||
convert, or every downstream consumer reads a 1.44x-wrong scale.
|
||||
153
scripts/sparse_mla_port/ab_vs_tilelang.py
Normal file
153
scripts/sparse_mla_port/ab_vs_tilelang.py
Normal file
@ -0,0 +1,153 @@
|
||||
"""FlagGems' `flash_mla_sparse_fwd` vs DeepInfer's TileLang sparse MLA, on the engine's own contract.
|
||||
|
||||
Why this pairing and not the DSA kernel: `attn_sm90.prefill_attn_varlen_sparse` calls the TileLang
|
||||
kernel with `is_causal=False`, documented in the source -- the indexer only scores positions before
|
||||
each query, so the indices carry causality, and bounding on the row index instead collapses the
|
||||
layer to NaN (that bug already happened once). `flash_mla_sparse_fwd` has no causal parameter for
|
||||
the same reason. The DSA kernel (`triton_sparse_mla_fwd_interface`) hardcodes causal and needs the
|
||||
V3.2 576-wide split layout, which V4.1 does not have, so it is not a candidate here.
|
||||
|
||||
Argument correspondence, which is closer than expected:
|
||||
|
||||
prefill_attn_varlen_sparse(query, kv, index_topk, softmax_scale,
|
||||
attn_sink, index_length)
|
||||
flash_mla_sparse_fwd(q, kv, indices, sm_scale,
|
||||
attn_sink=..., topk_length=...)
|
||||
|
||||
Both return (out, ...). `index_length` is a per-query valid count applied as -1 padding upstream;
|
||||
`topk_length` is the same thing applied inside the kernel.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import torch
|
||||
|
||||
# The vendored copy imports `flag_gems.utils...`; its stub sits next to it, so put that on the
|
||||
# path here rather than making the caller set PYTHONPATH for a layout detail.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent / "vendor"))
|
||||
|
||||
from vendor.fg_flashmla_sparse import flash_mla_sparse_fwd
|
||||
from deepinfer.h20_ops.sparse_mla_sm90 import sparse_mla_fwd_attn
|
||||
|
||||
DEV = "cuda"
|
||||
HQ, HKV, D, TOPK = 64, 1, 512, 512
|
||||
FAILED = []
|
||||
|
||||
|
||||
def tl_call(q3, kv3, idx4, scale):
|
||||
"""DeepInfer's shape convention: (S,H,D)/(S,1,D)/(S,1,topk) -> pad a batch axis."""
|
||||
s_q = q3.shape[0]
|
||||
s_kv = kv3.shape[0]
|
||||
out, lse = sparse_mla_fwd_attn(
|
||||
q3.view(1, s_q, HQ, D),
|
||||
kv3.view(1, s_kv, 1, D),
|
||||
idx4.view(1, s_q, 1, -1),
|
||||
sm_scale=scale,
|
||||
is_causal=False,
|
||||
)
|
||||
return out.view(s_q, HQ, D), lse.view(s_q, HQ)
|
||||
|
||||
|
||||
def compare(tag, a, b, extra=""):
|
||||
af, bf = a.float(), b.float()
|
||||
if af.shape != bf.shape:
|
||||
print(f" FAIL {tag}: shape {tuple(af.shape)} vs {tuple(bf.shape)}")
|
||||
FAILED.append(tag)
|
||||
return
|
||||
diff = (af - bf).abs()
|
||||
scale = af.abs().max().item()
|
||||
tol = 2.0**-8 * max(scale, 1e-9) * 2
|
||||
finite = bool(torch.isfinite(bf).all())
|
||||
ok = diff.max().item() <= tol and finite
|
||||
print(
|
||||
f" {'ok ' if ok else 'FAIL'} {tag}: max|ref| {scale:.4f} max diff {diff.max().item():.3e}"
|
||||
f" mean {diff.mean().item():.3e} tol {tol:.3e} finite {finite}{extra}"
|
||||
)
|
||||
if not ok:
|
||||
FAILED.append(tag)
|
||||
|
||||
|
||||
def make(SQ, SKV, seed, distinct):
|
||||
torch.manual_seed(seed)
|
||||
q = (torch.randn((SQ, HQ, D), device=DEV, dtype=torch.bfloat16) * 0.1).contiguous()
|
||||
kv = (torch.randn((SKV, HKV, D), device=DEV, dtype=torch.bfloat16) * 0.1).contiguous()
|
||||
if distinct and SKV > TOPK:
|
||||
rows = [torch.randperm(SKV, device=DEV)[:TOPK] for _ in range(SQ)]
|
||||
idx = torch.stack(rows).view(SQ, 1, TOPK).to(torch.int32)
|
||||
else:
|
||||
idx = torch.randint(0, SKV, (SQ, 1, TOPK), device=DEV, dtype=torch.int32)
|
||||
return q, kv, idx.contiguous()
|
||||
|
||||
|
||||
def case(tag, SQ, SKV, seed, distinct=True, use_length=False, use_sink=False):
|
||||
q, kv, idx = make(SQ, SKV, seed, distinct)
|
||||
scale = D**-0.5
|
||||
print(f"\n[{tag}] SQ={SQ} SKV={SKV} topk={TOPK} distinct={distinct}"
|
||||
f"{' +topk_length' if use_length else ''}{' +attn_sink' if use_sink else ''}")
|
||||
|
||||
# DeepInfer applies `index_length` by masking the tail of the index row to -1 upstream
|
||||
# (`_mask_beyond_length`), so the reference call is the same kernel with a padded index.
|
||||
idx_ref = idx
|
||||
tl_len = None
|
||||
if use_length:
|
||||
torch.manual_seed(seed + 100)
|
||||
tl_len = torch.randint(64, TOPK, (SQ,), device=DEV, dtype=torch.int32)
|
||||
idx_ref = idx.clone()
|
||||
ar = torch.arange(TOPK, device=DEV).view(1, 1, TOPK)
|
||||
idx_ref[(ar >= tl_len.view(SQ, 1, 1)).expand(SQ, 1, TOPK)] = -1
|
||||
idx_ref = idx_ref.contiguous()
|
||||
|
||||
tl_out, _ = tl_call(q, kv, idx_ref, scale)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
sink = None
|
||||
if use_sink:
|
||||
torch.manual_seed(seed + 200)
|
||||
sink = (torch.randn((HQ,), device=DEV, dtype=torch.float32) * 0.5)
|
||||
# DeepInfer's sink is folded into the output after the kernel; feed the kernel no sink and
|
||||
# compare the raw attention, then check the sink formula separately below.
|
||||
fg_out, fg_max, fg_lse = flash_mla_sparse_fwd(
|
||||
q, kv, idx_ref, sm_scale=scale, d_v=D,
|
||||
attn_sink=sink,
|
||||
topk_length=tl_len if use_length else None,
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
|
||||
if use_sink:
|
||||
# Compare against the reference with the sink applied the same way the kernel documents:
|
||||
# out * exp(lse) / (exp(lse) + exp(sink)).
|
||||
tl_lse = sparse_mla_fwd_attn(
|
||||
q.view(1, SQ, HQ, D), kv.view(1, SKV, 1, D), idx_ref.view(1, SQ, 1, -1),
|
||||
sm_scale=scale, is_causal=False,
|
||||
)[1].view(SQ, HQ)
|
||||
import math
|
||||
w = torch.exp(tl_lse.float() * math.log(2)) # TileLang lse is log2 units
|
||||
# tl_out is (SQ, HQ, D), so the weight must gain a trailing axis to broadcast over D.
|
||||
s = sink.view(1, HQ, 1)
|
||||
gate = w.unsqueeze(-1) / (w.unsqueeze(-1) + torch.exp(s))
|
||||
applied = tl_out.float() * gate
|
||||
compare("sink applied", applied, fg_out)
|
||||
else:
|
||||
compare("out", tl_out, fg_out)
|
||||
|
||||
|
||||
CASES = [
|
||||
("prefill, distinct top-k", dict(SQ=64, SKV=2048, seed=0)),
|
||||
("prefill, duplicate top-k", dict(SQ=32, SKV=512, seed=1, distinct=False)),
|
||||
("decode (SQ=1)", dict(SQ=1, SKV=4096, seed=2)),
|
||||
("odd SQ", dict(SQ=7, SKV=1024, seed=3)),
|
||||
("short KV (< topk)", dict(SQ=16, SKV=128, seed=4, distinct=False)),
|
||||
("with topk_length", dict(SQ=32, SKV=2048, seed=5, use_length=True)),
|
||||
("with attn_sink", dict(SQ=16, SKV=1024, seed=6, use_sink=True)),
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run from the repo root: PYTHONPATH=. python scripts/sparse_mla_port/ab_vs_tilelang.py
|
||||
for tag, kw in CASES:
|
||||
case(tag, **kw)
|
||||
print()
|
||||
if FAILED:
|
||||
print(f"MISMATCH {len(FAILED)}: {FAILED}")
|
||||
sys.exit(1)
|
||||
print(f"ALL_MATCH ({len(CASES)} cases)")
|
||||
55
scripts/sparse_mla_port/probe.py
Normal file
55
scripts/sparse_mla_port/probe.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""Can FlagGems' Triton sparse-MLA kernel run on H20 (SM90) at DeepSeek-V4.1's shape?
|
||||
|
||||
Deliberately narrow: first prove the kernel compiles and produces finite output, then (in
|
||||
sparse_mla_ab.py) compare it against the TileLang kernel DeepInfer uses today. Mixing the two into
|
||||
one script makes a compile failure look like a numerical mismatch.
|
||||
|
||||
Shape comes from the real config: 64 query heads, 1 kv head, head_dim 512, index_topk 512.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import traceback
|
||||
|
||||
import torch
|
||||
|
||||
# The vendored copy imports `flag_gems.utils...`; its stub sits next to it, so put that on the
|
||||
# path here rather than making the caller set PYTHONPATH for a layout detail.
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent / "vendor"))
|
||||
|
||||
from vendor.fg_flashmla_sparse import flash_mla_sparse_fwd
|
||||
|
||||
torch.manual_seed(0)
|
||||
DEV = "cuda"
|
||||
|
||||
B, SQ, HQ, HKV, D, TOPK, SKV = 1, 8, 64, 1, 512, 512, 64
|
||||
|
||||
q = torch.randn((B, SQ, HQ, D), device=DEV, dtype=torch.bfloat16) * 0.1
|
||||
kv = torch.randn((B, SKV, HKV, D), device=DEV, dtype=torch.bfloat16) * 0.1
|
||||
|
||||
# Valid indices into SKV, no duplicates within a row is not required by the kernel.
|
||||
idx = torch.randint(0, SKV, (B, SQ, HKV, TOPK), device=DEV, dtype=torch.int32)
|
||||
|
||||
# The kernel takes varlen shapes: fold the batch dim away (B is 1 here).
|
||||
q2 = q.view(B * SQ, HQ, D).contiguous()
|
||||
kv2 = kv.view(B * SKV, HKV, D).contiguous()
|
||||
idx2 = idx.view(B * SQ, HKV, TOPK).contiguous()
|
||||
|
||||
print(f"q {tuple(q2.shape)} kv {tuple(kv2.shape)} idx {tuple(idx2.shape)}", flush=True)
|
||||
print("h_q=64, h_kv=1, d=512 -- all inside the kernel's asserts", flush=True)
|
||||
|
||||
try:
|
||||
out, max_logits, lse = flash_mla_sparse_fwd(
|
||||
q2, kv2, idx2, sm_scale=D**-0.5, d_v=D
|
||||
)
|
||||
torch.cuda.synchronize()
|
||||
print(f"OK out {tuple(out.shape)} {out.dtype}")
|
||||
print(f" max_logits {tuple(max_logits.shape)} {max_logits.dtype}")
|
||||
print(f" lse {tuple(lse.shape)} {lse.dtype}")
|
||||
print(f" finite: out={torch.isfinite(out).all().item()} "
|
||||
f"lse={torch.isfinite(lse).all().item()}")
|
||||
print(f" mean |out| {out.float().abs().mean().item():.6f}")
|
||||
except Exception:
|
||||
print("FAIL")
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
1206
scripts/sparse_mla_port/vendor/fg_flashmla_sparse.py
vendored
Normal file
1206
scripts/sparse_mla_port/vendor/fg_flashmla_sparse.py
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
scripts/sparse_mla_port/vendor/flag_gems/__init__.py
vendored
Normal file
1
scripts/sparse_mla_port/vendor/flag_gems/__init__.py
vendored
Normal file
@ -0,0 +1 @@
|
||||
"""Minimal stand-in so the FlagGems fused kernels can be imported standalone."""
|
||||
0
scripts/sparse_mla_port/vendor/flag_gems/utils/__init__.py
vendored
Normal file
0
scripts/sparse_mla_port/vendor/flag_gems/utils/__init__.py
vendored
Normal file
27
scripts/sparse_mla_port/vendor/flag_gems/utils/triton_version_utils.py
vendored
Normal file
27
scripts/sparse_mla_port/vendor/flag_gems/utils/triton_version_utils.py
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
"""Stand-in for FlagGems' version probe.
|
||||
|
||||
The real module gates TLE (Triton Language Extensions) on the triton version. Here it reports
|
||||
whatever the installed triton actually supports, because the point is running the kernel, not
|
||||
reproducing FlagGems' packaging.
|
||||
"""
|
||||
|
||||
import triton
|
||||
|
||||
|
||||
def _version_tuple() -> tuple[int, ...]:
|
||||
parts = []
|
||||
for piece in triton.__version__.split(".")[:3]:
|
||||
digits = "".join(c for c in piece if c.isdigit())
|
||||
parts.append(int(digits) if digits else 0)
|
||||
return tuple(parts)
|
||||
|
||||
|
||||
def has_triton_tle(*minimum: int) -> bool:
|
||||
try:
|
||||
import triton.experimental.tle.language # noqa: F401
|
||||
except Exception:
|
||||
return False
|
||||
return _version_tuple() >= tuple(minimum)
|
||||
|
||||
|
||||
HAS_TLE = has_triton_tle(3, 6, 0)
|
||||
Loading…
x
Reference in New Issue
Block a user