Skip to content

[Bench][Linear-Attn] Add Qwen GDN prefill benchmark surface#5

Draft
superAngGao wants to merge 35 commits into
impl/gdn-prefill-opfrom
issue-1597-gdn-prefill-bench
Draft

[Bench][Linear-Attn] Add Qwen GDN prefill benchmark surface#5
superAngGao wants to merge 35 commits into
impl/gdn-prefill-opfrom
issue-1597-gdn-prefill-bench

Conversation

@superAngGao

Copy link
Copy Markdown
Owner

Stacked on tile-ai#1596. Addresses tile-ai#1597.

Summary

  • Switch the Gated DeltaNet prefill benchmark surface to the FLA/Qwen BTHD contract.
  • Add Qwen3.5-aligned long-context rows for S=32K/64K/128K and H=16/32/48/64 across fp16/bf16.
  • Compare TileOps against FLA with output_final_state=True and surface speedup in profile_run.log.
  • Keep nightly JUnit properties compatible while allowing profile_run.log to show extra metrics and GPU clocks.

Validation

  • python -m py_compile benchmarks/ops/bench_gated_deltanet_prefill.py workloads/gated_deltanet.py benchmarks/benchmark_base.py tileops/perf/formulas.py
  • python scripts/validate_manifest.py
  • python -m pytest --collect-only benchmarks/ops/bench_gated_deltanet_prefill.py -q
  • TMPDIR=/home/ga/tmp python -m pytest tests/ops/test_gated_deltanet_prefill.py -m smoke -q

Notes

  • The local environment does not have FLA installed, so full benchmark execution was only checked for collection/skip behavior here.

@github-actions github-actions Bot added the bench Auto-created by labeler label Jun 23, 2026
@superAngGao superAngGao force-pushed the impl/gdn-prefill-op branch 3 times, most recently from 9d21d88 to 9282b2b Compare June 24, 2026 10:51
zhen8838 and others added 26 commits June 25, 2026 15:59
…16, causal) (tile-ai#1614)

## Summary

Adds `GQAPrefillFwdWsPersistentCausalKernel` — a faithful port of
FlashInfer's `single_prefill_sm90` — and wires it into
`GroupedQueryAttentionPrefillFwdOp`. On Hopper, the prefill op now
dispatches to this kernel for the **fp16 / causal / dim==128 / default
sm_scale / no-softcap** path, reaching FlashInfer-level performance; all
other cases fall back to the existing `GQAPrefillFwdKernel`.

## Performance

Kernel-level parity with FlashInfer. Per-kernel CUDA time from
`benchmarks/ops/attention/bench_gqa.py` (H200, fp16, causal, L2 flushed
per iteration; `single_prefill_sm90` is the kernel this ports):

| batch · Sq · Skv | tileops kernel | flashinfer kernel |
tileops/flashinfer |
| --- | --- | --- | --- |
| 4 · 512 · 512 | 42.0 µs | 40.4 µs | 1.04x |
| 2 · 512 · 4096 | 122.6 µs | 126.2 µs | 0.97x |
| 1 · 512 · 4096 | 122.0 µs | 125.3 µs | 0.97x |

Within ~3–4% across the workloads: at parity / marginally faster on long
context (Skv=4096, ~525 TFLOPS), marginally slower on the short square
case (Skv=512). The bench now reports per-kernel CUPTI time directly —
it wraps the timed call in a `record_function` scope and sums only the
kernels Kineto attributes to it, so the per-iteration L2 flush is
excluded from the measurement.

## Kernel

2-warpgroup ping-pong (1 TMA producer WG + 2 consumer WG, 384 threads);
register-P (rs-wgmma) held in a fp16 fragment; `set_max_nreg`
producer→24 / consumer→240; named-barrier WarpScheduler ping-pong;
delayed PV + `wait_wgmma(1)` so softmax(k) overlaps in-flight PV(k-1);
pipelined rescale; smem-staged coalesced output. Causal uses
bottom-right alignment (`co = seq_len_kv - seq_len_q`).

## Notes

- **No log-sum-exp output**, matching FlashInfer's forward kernel. The
op's `(output, lse)` contract is satisfied with `lse=None`. Emitting lse
from the epilogue perturbed the register-tuned schedule and cost ~35% on
long context; a backward pass that needs lse must compute it separately.
- `bench_gqa`: the flashinfer baseline helper is generalized to handle
`Sq != Skv` and is now recorded for the prefill bench.
- Manifest `kernel_map` / `source.kernel` are intentionally **not**
touched here — adding a new-file kernel requires a `source.kernel`
change, which is outside the implementation-PR carve-out and will follow
as a separate manifest PR.

## Verification

- 22/22 prefill-fwd correctness tests pass (fp16 → FA3 kernel; bf16 /
non-causal / softcap / non-default scale → fallback).
- 6/6 prefill bench cases pass.
- `ruff` clean.
)

## Summary

Fixes tile-ai#1617 - GLA decode benchmark timeout

This PR resolves the kernel compilation issue that prevented GLA decode
from working. The root cause was intermediate variable assignments
inside `T.Parallel(k_tile, dim_v)` loops within `T.Pipelined()`, which
created `tirx.Bind` IR nodes that TileLang's warp-specialization pass
couldn't handle.

## Changes

### Kernel Compilation Fix (`tileops/kernels/gla_recurrence.py`)

**Root cause**: Intermediate variables inside `T.Parallel(k_tile,
dim_v)` loops within `T.Pipelined()` created `tirx.Bind` IR nodes that
TileLang's warp-specialization pass couldn't handle.

**Solution**:
- Preload `k`, `v`, `gk` tensors into shared memory before the pipelined
loop
- Inline all computations in the pipelined loop (eliminate intermediate
variables)
- Applied to both `_gla_decode_tl()` (fp16/bf16) and
`_gla_decode_fp32_tl()`

**Before** (BROKEN):
```python
for kt in T.Pipelined(dim_k // k_tile, num_stages=num_stages):
    T.copy(state[bid, hid, kt * k_tile, 0], h_tile)
    for kk, j in T.Parallel(k_tile, dim_v):
        gk_val = gk[bid, hid, kt * k_tile + kk]  # ← Intermediate variable
        alpha_i = T.exp2(gk_val * _LOG2E)         # ← Creates BindNode
        new_state[...] = alpha_i * h_tile[kk, j] + ...
```

**After** (FIXED):
```python
# Preload to shared memory
k_shared = T.alloc_shared([dim_k], dtype)
gk_shared = T.alloc_shared([dim_k], dtype)
v_shared = T.alloc_shared([dim_v], dtype)

for i in T.Parallel(dim_k):
    k_shared[i] = k[bid, hid, i]
    gk_shared[i] = gk[bid, hid, i]
for i in T.Parallel(dim_v):
    v_shared[i] = v[bid, hid, i]

# Inline computations - no intermediate variables
for kt in T.Pipelined(dim_k // k_tile, num_stages=num_stages):
    T.copy(state[bid, hid, kt * k_tile, 0], h_tile)
    for kk, j in T.Parallel(k_tile, dim_v):
        new_state[...] = (
            T.exp2(gk_shared[kt * k_tile + kk] * _LOG2E) * h_tile[kk, j]
            + k_shared[kt * k_tile + kk] * v_shared[j]
        )
```

## Testing

All GLA decode tests pass:

```bash
tests/ops/test_gla_recurrence.py::test_gla_decode[1-4-64-64-dtype0-False] PASSED
tests/ops/test_gla_recurrence.py::test_gla_decode[1-4-64-64-dtype1-False] PASSED
tests/ops/test_gla_recurrence.py::test_gla_decode[1-4-64-64-dtype2-False] PASSED
tests/ops/test_gla_recurrence.py::test_gla_decode[2-8-64-64-dtype3-False] PASSED
tests/ops/test_gla_recurrence.py::test_gla_decode[2-4-128-128-dtype4-False] PASSED
tests/ops/test_gla_recurrence.py::test_gla_decode[2-8-64-64-dtype5-False] PASSED
tests/ops/test_gla_recurrence.py::test_gla_decode[2-8-64-64-dtype6-False] PASSED
```

✅ Kernels compile successfully for all dtypes (fp32, fp16, bf16)  
✅ No NaN/Inf in outputs  
✅ Benchmark unblocked

## Impact

- **Performance**: May improve due to reduced global memory traffic;
shared memory usage increases by ~768 bytes per block (dim_k=64,
dim_v=64, fp32)
- **Pattern alignment**: Now matches DeltaNet/GatedDeltaNet coding
patterns
- **Correctness**: All tests pass with proper numerical results

## Environment Note

The test reference implementation uses `torch.einsum`. This requires
PyTorch built with matching CUDA version (e.g., PyTorch 2.10.0+cu129 for
CUDA 12.9). If you encounter CUBLAS errors with batch operations, ensure
your PyTorch CUDA variant matches your system CUDA version.

## Benchmark Results 🚀

Benchmarked on **NVIDIA H200** (CUDA 12.9, PyTorch 2.10.0+cu129)

### Performance Comparison: TileOPs vs FLA (Flash Linear Attention)

| Config | dtype | TileOPs (ms) | FLA (ms) | Speedup |
|--------|-------|--------------|----------|---------|
| B=1, H=32, D=64 | fp32 | 0.0080 | 0.0071 | **0.89x** ⚠️ |
| B=1, H=32, D=128 | fp32 | 0.0143 | 0.0080 | **0.56x** ⚠️ |
| B=1, H=32, D=128 | fp16 | 0.0143 | 0.0089 | **0.62x** ⚠️ |
| B=1, H=32, D=128 | bf16 | 0.0139 | 0.0090 | **0.65x** ⚠️ |
| B=8, H=32, D=128 | fp32 | 0.0302 | 0.0215 | **0.71x** ⚠️ |
| B=8, H=32, D=128 | fp16 | 0.0313 | 0.0187 | **0.60x** ⚠️ |
| B=8, H=32, D=128 | bf16 | 0.0298 | 0.0188 | **0.63x** ⚠️ |
| B=16, H=32, D=128 | fp32 | 0.0831 | 0.0372 | **0.45x** ⚠️ |
| B=16, H=32, D=128 | fp16 | 0.0785 | 0.0284 | **0.36x** ⚠️ |
| B=16, H=32, D=128 | bf16 | 0.0761 | 0.0285 | **0.37x** ⚠️ |
| B=32, H=32, D=128 | fp32 | 0.1889 | 0.0670 | **0.35x** ⚠️ |
| B=32, H=32, D=128 | fp16 | 0.1828 | 0.0470 | **0.26x** ⚠️ |
| B=32, H=32, D=128 | bf16 | 0.1752 | 0.0472 | **0.27x** ⚠️ |
| B=64, H=32, D=128 | fp32 | 0.4045 | 0.1318 | **0.33x** ⚠️ |
| B=64, H=32, D=128 | fp16 | 0.3890 | 0.0841 | **0.22x** ⚠️ |
| B=64, H=32, D=128 | bf16 | 0.3772 | 0.0846 | **0.22x** ⚠️ |

### Analysis

**Current Status:**
- ⚠️ TileOPs is currently **slower** than FLA by 1.1x-4.6x across all
configurations
- FLA achieves **0.9-1.6 TFLOPS** vs TileOPs' **0.07-0.6 TFLOPS**
- The gap widens with larger batch sizes (B=64: 4.5x slower)

**Performance Gap:**
- **Small batches (B=1)**: 11-78% slower
- **Medium batches (B=8-16)**: 2.2-2.8x slower  
- **Large batches (B=32-64)**: 2.8-4.6x slower

**Key Observations:**
1. ✅ **Correctness verified**: All tests pass against FLA reference
2. ✅ **Kernel compiles successfully**: Fixed BindNode compilation issue
3. ⚠️ **Performance needs optimization**: Current implementation is
functional but not yet competitive with FLA's highly optimized kernels

### Next Steps for Performance

This PR successfully **unblocks GLA decode** by fixing the compilation
issue. Performance optimization can be addressed in follow-up work:

1. **Tuning opportunities**: Adjust `num_stages`, `k_tile`, thread
counts
2. **Memory access patterns**: Further optimize shared memory usage
3. **Warp-level optimizations**: Leverage H200's compute capabilities
4. **Benchmark-driven iteration**: Profile with NSight Compute to
identify bottlenecks
## Summary

Fixes tile-ai#1613.

TileLang 0.1.11 validates/binds autotuned JIT signatures before
candidate configs are applied. Several TileOPs kernels expose tunable
JIT parameters such as `block_m`, `block_n`, and `bdim` as required
arguments, so calling the autotuned wrapper with no kwargs can fail
before autotuning starts.

This seeds the autotuned wrapper call with bindable config values while
preserving the existing autotune search space.

## Changes

- Add shared `Kernel._call_autotuned_kernel()` /
`_autotune_initial_kwargs()` helpers.
- Seed base `Kernel.autotune()` with `default_config`, filtered to the
JIT signature.
- Support common JIT parameter aliases such as `threads_arg` and
`npt_arg`.
- Update custom autotune overrides that previously called autotuned
wrappers with no kwargs:
  - `SoftmaxKernel`
  - `LogSumExpKernel`
  - `SparseMlaKernel`
  - `FP8LightingIndexerKernel`
  - DeltaNet / GatedDeltaNet sub-kernel autotune paths
- Add unit coverage for required tunable binding, signature filtering,
and alias mapping.

## Validation

- `python3 -m pytest -q tests/kernels/test_kernel_autotune_binding.py`
- `python3 -m ruff check tileops/kernels/kernel_base.py
tileops/kernels/reduction/softmax.py
tileops/kernels/reduction/logsumexp.py
tileops/kernels/attention/deepseek_dsa_decode.py
tileops/kernels/fp8_lighting_indexer.py
tileops/kernels/deltanet/deltanet_fwd.py
tileops/kernels/deltanet/deltanet_bwd.py
tileops/kernels/gated_deltanet/gated_deltanet_fwd.py
tests/kernels/test_kernel_autotune_binding.py`
- `python3 -m compileall -q ...` on changed Python files
- commit hooks / pre-commit checks

Note: local environment has TileLang 0.1.9, so the regression test mocks
the TileLang 0.1.11-style binding requirement instead of running the
exact 0.1.11 autotuner.
…al handling (tile-ai#1626)

Fixes tile-ai#1625.

## Summary

Resolves the nightly Triton/Inductor `PermissionError` under `/ci-cache`
by establishing a UID-agnostic ownership invariant for the shared CI
build cache, plus a related runner stop-signal fix and the gpu-smoke
cache de-namespacing.

### Cache ownership

- `.github/runner/Dockerfile`: pin `ci-runner` to a deterministic
UID/GID 1000 (`groupadd -g 1000 ci-runner && useradd -u 1000 -g 1000
...`) so the account deterministically owns the shared `/ci-cache`
bind-mount across image rebuilds.
- The `chown -R ci-runner:ci-runner ... /ci-cache` line is retained but
documented as serving only the **unmounted** image-verification path; in
the mounted CI path the host bind-mount shadows those dirs (host
ownership wins).
- `.github/runner/entrypoint.sh`: set `umask 002` before the runner
agent starts so compiled kernels land group-writable (`0775`/`2775`) for
cross-UID cache reuse.

### Runner stop-signal handling

- `.github/runner/entrypoint.sh`: `exec ./run.sh` + `export
RUNNER_MANUALLY_TRAP_SIG=1`.
- **Why:** `docker stop` (and the launcher's `systemctl stop`) delivers
SIGTERM only to PID 1. With `./run.sh` as a foreground child, PID 1 was
the entrypoint bash, which defers its trap while waiting and never
relays the signal; an idle ephemeral runner then ignored SIGTERM and was
SIGKILLed only after docker's full stop grace (~30 min via the
launcher's `docker stop -t 1800`).
- **Fix:** `exec` makes `run.sh` PID 1 so docker's SIGTERM reaches it;
`RUNNER_MANUALLY_TRAP_SIG=1` makes `run.sh` trap SIGTERM/SIGINT and
forward SIGINT to the listener process group for graceful shutdown.
- **Trade-off:** a stop signal now cancels an in-progress job rather
than draining it.

### gpu-smoke cache de-namespacing

- `.github/workflows/gpu-smoke.yml`: drop the per-run
`TRITON_CACHE_DIR=/ci-cache/triton/gpu-smoke-<run>-<attempt>` step so
gpu-smoke shares the warm Triton cache like nightly. `TRITON_CACHE_DIR`
falls back to the image ENV default `/ci-cache/triton`. This is safe now
that ownership is UID-agnostic (above) and fork write isolation lives at
the runner's overlay mount, not the workflow. It also stops the per-run
dirs accumulating.
- `nightly.yml` and `scripts/ci/prepare_cache_dirs.sh` need no change:
the earlier run-local-cache hotfix (PR tile-ai#1618) was never merged, so
nightly already uses the shared cache and `prepare_cache_dirs.sh` does
not exist on `main`.

## Test plan

- [x] Dockerfile pins `ci-runner` to UID/GID 1000; `entrypoint.sh` sets
`umask 002`
- [x] Image rebuilt (same tilelang commit / tag) and deployed on the
runner host; verified `id ci-runner` = `1000:1000`, `umask` = `0002`
- [x] Host cache normalized (group `1000` + group-writable + setgid); a
container `ci-runner` can write into a previously-foreign cache dir (was
`Permission denied`)
- [x] Idle runner `docker stop` returns in ~0.3s (was ~30 min) after the
signal fix
- [x] gpu-smoke.yml parses; `TRITON_CACHE_DIR` resolves to the shared
`/ci-cache/triton` via image ENV
- [ ] Nightly op tests (GLA / Mamba / MoE) pass the Triton/Inductor
cache step with no `PermissionError` (final end-to-end confirmation)
- [x] pre-commit passed on changed files

---------

Co-authored-by: Ibuki 🍃 — a wind born from GPTs <Ibuki-wind@users.noreply.github.com>
…gsumexp kernels under TileLang 0.1.11 (tile-ai#1627)

## Summary

Fixes autotune failures under TileLang 0.1.11 for three kernel families
that were not covered by PR tile-ai#1619.

### Root cause

TileLang 0.1.11 changed its autotuner: when
`autotuned_kernel_fn(**initial_kwargs)` is called with the tunable JIT
parameters already bound, it records those params in the cache key and —
on a cache hit — treats them as "already tuned," skipping the
benchmarking sweep entirely and returning `config=None`.

PR tile-ai#1619 added `_call_autotuned_kernel()` to seed required JIT params
(fixing the `TypeError: missing N required positional arguments`), but
this inadvertently triggered the new 0.1.11 skip behavior, causing:
- `SSDStatePassingFwdKernel` / `SSDChunkScanFwdKernel` /
`SSDDecodeKernel`: `Best config: None`
- `SoftmaxKernel` / `LogSumExpKernel`: `TypeError: '<' not supported
between instances of 'NoneType' and 'float'`

### Fix

Pass `do_not_specialize=tunable_params` to TileLang's `autotune()`
decorator. This tells 0.1.11 to exclude those parameter names from the
cache key, so the seeded initial values no longer trigger the skip path
— the full benchmarking sweep runs as expected.

## Changes

- **`tileops/kernels/kernel_base.py`** (`Kernel.autotune()`): extract
tunable param names from the JIT signature and pass as
`do_not_specialize`. Fixes `SSDStatePassingFwdKernel`,
`SSDChunkScanFwdKernel`, `SSDDecodeKernel`, and any future kernel that
uses the base `autotune()`.

- **`tileops/kernels/reduction/softmax.py`**
(`SoftmaxKernel.autotune()`): same pattern for the custom per-tile_n
autotune loop.

- **`tileops/kernels/reduction/logsumexp.py`**
(`LogSumExpKernel.autotune()`): same pattern for the custom per-tile_n
autotune loop.

## Validation

Tested against real TileLang 0.1.11 (`0.1.11+cuda.gitcd37ed5f`) in
`flashmlaenv`:

**Mamba kernels** — all run full sweeps and return valid configs:
- `SSDStatePassingFwdKernel` → `{'block_d': 64, 'threads': 128,
'vectorize': False}`
- `SSDChunkScanFwdKernel` → `{'block_l': 64, 'block_p': 64, 'block_n':
64, 'block_s': 64, 'threads': 128}`
- `SSDDecodeKernel` → `{'block_p': 4, 'block_n': 32, 'threads': 128}`

**Softmax/logsumexp** — `tests/ops/test_softmax.py`: **135 passed**
(previously 3 failed with `TypeError`)

**Autotune binding unit tests** —
`tests/kernels/test_kernel_autotune_binding.py`: **3 passed**

**deepseek_dsa_decode** —
`tests/ops/attention/test_deepseek_dsa_decode.py`: **1 passed**

**Ruff** — all PR tile-ai#1619 validation files pass cleanly.
…try (tile-ai#1629)

## Summary

Makes dense `GemmOp` **input-inferred** and brings it under the
manifest, aligning with
DeepGEMM's call-time-JIT model. Implements step 1 of tile-ai#1628.

- `GemmOp` no longer commits shape/dtype at construction: `forward(a,
b)` infers `m/n/k`
and `dtype` from the inputs, builds & caches the dtype-specialized
kernel per
`(m, n, k, dtype)` on first use (mirroring DeepGEMM's
compile-on-first-call + per-config
  cache), and returns a freshly-allocated `d`.
- Default layout changed **NN → NT** (`trans_b=True`) to match
DeepGEMM's primary path and
  `GroupedGemmOp`. `(trans_a, trans_b)` ≡ DeepGEMM `nt/nn/tn/tt`.
- New `tileops/manifest/gemm.yaml` with the `GemmOp` entry (`status:
implemented`).
- GEMV fast-path dispatch (`m==1`/`n==1` on SM90) moved into `forward`.

## Verification (H200, container stack)

- `scripts/validate_manifest.py` → exit 0, no GemmOp errors.
- `pytest tests/ops/test_gemm.py` → **22 passed** (incl. NN/NT/TN/TT
layouts, GEMV boundary,
  fp16/bf16, and the `tune=True` case).
- `pytest benchmarks/ops/bench_gemm.py` → 4 passed (manifest-driven,
`eval_roofline`-based,
  cuBLAS baseline).
- `ruff check` clean on all changed Python files.

## Deviations from the issue plan (please review)

1. **Roofline is func mode, not inline.** A single static inline `vars`
expression can't
extract `M/N/K` correctly across all `trans_a/trans_b` layouts, and
`M/N/K` can't be
declared as manifest params (they're neither `__init__` nor `forward`
args, so the
signature check would reject them). Func mode
(`tileops.ops.gemm._gemm_roofline`, reads
`op.m/n/k/dtype` bound during `forward`) is correct for every layout.
`eval_roofline()` is
   therefore valid only after the first `forward()`.

2. **Includes a small kernel autotune fix (`tileops/kernels/gemm.py`).**
`tune=True` was
broken for *both* `GemmKernel` and `GemvKernel` on the current tilelang
stack: the
`@tilelang.jit` builders had no parameter defaults, so the autotuner's
validation
`get_tir()` call (empty args) raised `missing required argument
'block_m'/'block_n'`. Fix:
added default values to the tunable builder params. This is scoped to
the GEMM/GEMV
builders only — it does **not** touch the shared
`kernel_base.autotune()` path, so other
ops' autotune (also affected by the same upstream tilelang drift) is
intentionally left for
a separate tracking issue. (Authorized expansion beyond the issue's "no
kernel" constraint.)

3. **Trust-model split.** The manifest entry is isolated in its own
commit
(`[Manifest][GEMM] …`) so it can be cherry-picked into a standalone
human-reviewed
manifest-only PR before merge, per `docs/design/trust-model.md`. This
draft bundles it with
   the implementation only for review convenience.

## Benchmark (H200, torch 2.10.0+cu129)

`pytest benchmarks/ops/bench_gemm.py` on an **idle GPU**
(`CUDA_VISIBLE_DEVICES` pinned to a 0%-util
device; SM clock locked at 1500 MHz) — TileOPs `GemmOp` (default
cp.async `GemmKernel`) vs
`torch.matmul` (cuBLAS), via the repo's CUPTI / cold-L2 profiler.
Speedup = cuBLAS latency ÷ TileOPs
latency (>1 = TileOPs faster).

| m | n | k | layout | dtype | TileOPs (ms / TFLOPS) | cuBLAS (ms /
TFLOPS) | speedup |
|---|---|---|---|---|---|---|---|
| 1024 | 1024 | 1024 | NN | fp16 | 0.0165 / 130 | 0.0075 / 288 | 0.45× |
| 1024 | 1024 | 1024 | NN | bf16 | 0.0165 / 130 | 0.0075 / 288 | 0.45× |
| 128 | 2112 | 7168 | NT | bf16 | 0.1146 / 34 | 0.0184 / 211 | 0.16× |
| 128 | 7168 | 2048 | NT | bf16 | 0.0289 / 130 | 0.0135 / 278 | 0.47× |
| 4096 | 2112 | 7168 | NT | bf16 | 0.4437 / 280 | 0.3062 / 405 | 0.69× |
| 4096 | 7168 | 2048 | NT | bf16 | 0.3241 / 371 | 0.3132 / 384 | 0.97× |
| 4096 | 4096 | 7168 | NT | fp16 | 0.6130 / 392 | 0.6242 / 385 | 1.02× |
| 4096 | 4096 | 7168 | NT | bf16 | 0.6078 / 396 | 0.6078 / 396 | 1.00× |
| 4096 | 7168 | 16384 | NT | bf16 | 2.5455 / 378 | 2.5278 / 381 | 0.99×
|
| 4096 | 24576 | 1536 | NT | bf16 | 0.9270 / 334 | 0.8552 / 362 | 0.92×
|

**Takeaway:** at cuBLAS parity (0.92–1.02×) on large compute-bound
prefill shapes (m=4096, K-aligned),
but meaningfully slower on small / decode-token shapes (m ≤ 1024 →
0.16–0.47×) and on non-256-aligned
N (4096×2112×7168 → 0.69×), where the simple cp.async `GemmKernel` is
launch/memory/tail-bound. This
refactor preserves the existing kernel's numerics and perf — closing the
small-m and tile-alignment
gaps is what the follow-up persistent / split-k work targets.

## Out of scope (follow-ups)

- `GemmAccumulateOp` (optional `c`/accumulate as a `variant_of` class).
- persistent / split-k / stream-k kernels.
- fp8/fp4 GEMM sibling op.
- grouped / gemv manifest entries.
- repo-wide `kernel_base.autotune()` fix for the tilelang API drift.
…tune kernels (tile-ai#1631)

## Summary

Follow-up to tile-ai#1627, which fixed the TileLang 0.1.11 autotune-skip bug
(seeded tunable params recorded in the cache key → treated as "already
tuned" → sweep skipped, `config=None` → crash) in the base
`Kernel.autotune()` and in `SoftmaxKernel` / `LogSumExpKernel`.

These custom-autotune kernels build the TileLang `autotune(...)`
decorator directly and were not covered:
- `deltanet_fwd` / `deltanet_bwd` (per sub-kernel)
- `gated_deltanet_fwd` (per sub-kernel)
- `deepseek_dsa_decode` (`SparseMlaKernel`)

## Fix

- At each autotuned call site, derive the seeded tunable parameter names
via `_autotune_initial_kwargs` and pass them as `do_not_specialize` so
TileLang excludes them from the cache key and runs the full sweep
(guarded so it's only passed when non-empty).
- `deepseek_dsa_decode` also passes `supply_prog`: TileLang calls it
with the candidate JIT params, but `SparseMlaKernel.supply_prog` takes
none (it builds inputs from instance attrs), so it's wrapped (`lambda
*args, **kwargs: self.supply_prog()`) to discard the extra arg.

## Test coverage

Added one `tune=True` (`full` tier) regression case to
`test_deltanet_fwd.py`, `test_deltanet_chunkwise_bwd.py`,
`test_gated_deltanet_fwd.py`, and `test_deepseek_dsa_decode.py` so these
autotune paths are covered by CI.

## Not included — fp8_lighting_indexer

`fp8_lighting_indexer` was in scope initially but is **reverted**: its
`supply_prog(q, kv, ...)` needs tensors not available at tune time and
requires an instance-attr fallback (a separate change). Deferred to a
follow-up.

## Test plan

- [x] H200: `DeltaNetFwdKernel(tune=True)` and
`SparseMlaKernel(tune=True)` autotune and return a populated config
(were `None` / crash).
- [x] New tuned cases collect cleanly and satisfy the conftest tier
validator.
- [x] `ruff` + pre-commit pass.

### Test node delta
4 new nodes (one `tune=True` `full` case per touched test file).
`scripts/test_node_delta.py` could not run in the runner image
(host-owned workspace not writable by the container user); the growth is
exactly those 4 cases.
)

## Problem

`FP8LightingIndexerOp(..., tune=True)` crashed before ever producing a
valid config. The autotune path had **three independent defects**, all
in `FP8LightingIndexerKernel`:

1. **Skip-guard leaves `config=None`** — seeded tunable JIT params
landed in the autotuner cache key, so TileLang 0.1.11 treated the kernel
as *already tuned*, skipped the sweep, and left `self.config = None`.
Downstream `self.config[...]` then raised `TypeError`.
2. **`supply_prog` signature incompatible with the autotuner** —
TileLang calls `supply_prog(params)` positionally for each candidate,
but the signature was `(self, q, kv, dtype=..., ...)`, so every
candidate raised `TypeError` and the sweep found nothing.
3. **`supply_prog` built wrong inputs** even once callable:
- `torch.randn(..., dtype=torch.float8_e4m3fn)` → `NotImplementedError:
normal_kernel_cuda not implemented for Float8_e4m3fn`. Generate in fp16,
then cast.
- `IndexK` was cast to `self.dtype`; the kernel signature requires fp8 →
`IndexK dtype mismatch, expected float8_e4m3fn`.
- The `Logits` output buffer (kernel arg 4 of 7) was missing from the
returned tuple.

## Fix

- Pass `do_not_specialize=<tunable param names>` to `autotune(...)` so
seeded params stay out of the cache key (mirrors the central fix in
`kernel_base`).
- Rewrite `supply_prog` to `(self, *args, **kwargs)`, building inputs
from the kernel's shape attributes and returning all 7 kernel args
(incl. `Logits`).

## Test delta

`tests/ops/test_fp8_lighting_indexer.py`: +1 node — a `full`-tier
`tune=True` case tracing the repaired autotune path. `full`-tier only
(excluded from smoke PRs); the full 54-config sweep is the dominant cost
there.

---------

Co-authored-by: Ibuki 🍃 — a wind born from GPTs <Ibuki-wind@users.noreply.github.com>
…y race (tile-ai#1633)

## Summary

Fixes tile-ai#1616, plus a defensive fix for the same latent race in the plain
MHA/GQA forward WGMMA kernels.

The fixed-length `GQASlidingWindowFwdWgmmaPipelinedKernel` (selected on
H200) miscomputed non-causal right-window cases — up to ~19% of elements
wrong, max abs diff ~0.47 (tolerance 0.01), reproduced deterministically
across recent `main` gpu-smoke runs.

## Root cause

The kernel writes the output accumulator through a **swizzled**
`o_shared` buffer (`acc_o -> o_shared -> output`) with **no barrier**
around the round-trip. Across warps this is a shared-memory race: a warp
reads `o_shared` before peer warps finish writing it, corrupting the
output under WGMMA pipelining (`num_stages >= 2`).

The varlen sibling already guards the identical round-trip with
`T.sync_threads(3, threads)` (added in the closed tile-ai#1404 "wgmma varlen
output shared-memory race"); that guard was never ported to the
fixed-length kernel. This PR ports it.

**Why a named barrier (slot 3, not bare `T.sync_threads()`):** verified
on H200 — a bare `T.sync_threads()` (slot 0) aliases the implicit
`__syncthreads()` the surrounding `T.copy` lowering already emits, gets
elided, and the race survives. Any non-zero slot works.

**Why non-causal right-window specifically:** some query rows have a
fully-masked trailing KV tile, which shifts warp scheduling enough to
expose the race. Causal / left-only / no-window paths and `num_stages=1`
happened to avoid the timing window — which is why those passed and
masked the latent bug.

## Commits

1. `gqa_sliding_window_fwd.py` — the tile-ai#1616 fix.
2. `gqa_fwd.py` — defensive fix: `_mha_fwd_wgmma_pipelined_main` /
`_gqa_fwd_wgmma_pipelined_main` have the identical unguarded write-back.
Plain attention has no masked-tile trigger so it is latent (tests pass),
but the hazard is real; barriers only add synchronization.
3. comment clarifying the named-barrier requirement.

## Correctness verification (CI image
`ghcr.io/tile-ai/tileops-runner:65dbc98-torch2.10-dev`, torch
2.10.0+cu129, H200)

| suite | result |
|---|---|
| `tests/ops/attention/test_gqa_sliding_window.py` | **21/21 passed**
(incl. the two previously-failing cases) |
| `tests/ops/attention/test_mha.py` + `test_gqa.py` | **55 passed, 4
skipped** (no regression from commit 2) |

## Barrier perf impact — before/after latency (median over 100 iters,
same image)

The two `T.sync_threads` run once per block at the end (not in the KV
loop), so the cost is below run-to-run noise.

| kernel | shape | latency before (ms) | latency after (ms) |
|---|---|---|---|
| GQASlidingWindowFwdWgmmaPipelined | fp16 wl=-1 wr=64 | 0.0167 | 0.0164
|
| GQASlidingWindowFwdWgmmaPipelined | bf16 wl=64 wr=64 | 0.0149 | 0.0146
|
| MHAFwdWgmmaPipelined | causal fp16 | 0.0162 | 0.0159 |
| GQAFwdWgmmaPipelined | causal fp16 | 0.0160 | 0.0157 |
| MHAFwdWgmmaPipelined | causal bf16 | 0.0161 | 0.0159 |
| GQAFwdWgmmaPipelined | causal bf16 | 0.0158 | 0.0158 |

> "before" = the unguarded `origin/main` kernels (temporarily reverted,
then restored). The sliding-window "before" output is numerically wrong
(the bug); only latency is comparable.

## GQA forward vs reference baselines (`bench_gqa.py`, same image, H200)

Metric is **throughput in TFLOPS (higher is better)** — not latency.
TileOPs = `GroupedQueryAttentionFwdOp` (kernel variant in parens).
Baselines = FlashAttention-3 and FlashInfer. Bold = fastest in the row.

### Compute-bound (S=2048, causal, autotuned) — the representative
comparison

| model | dtype | TileOPs (TFLOPS) | FA3 (TFLOPS) | FlashInfer (TFLOPS)
|
|---|---|---|---|---|
| Llama-3.1-8B (B=2, H=32, Hkv=8, D=128) | fp16 | 412 (ws_causal) |
**508** | 215 |
| Llama-3.1-8B (B=2, H=32, Hkv=8, D=128) | bf16 | 107 (wgmma_pipelined)
| **288** | 186 |
| Llama-3.1-70B (B=1, H=64, Hkv=8, D=128) | fp16 | 212 (ws_causal) |
**236** | 216 |
| Llama-3.1-70B (B=1, H=64, Hkv=8, D=128) | bf16 | 146 (wgmma_pipelined)
| **236** | 186 |

### Small / overhead-bound (S=512, causal) — latency-dominated, not
throughput-representative

| model | dtype | TileOPs (TFLOPS) | FA3 (TFLOPS) | FlashInfer (TFLOPS)
|
|---|---|---|---|---|
| Llama-3.1-8B (B=4, H=32, Hkv=8, D=128) | fp16 | 215 (ws_causal) |
**249** | 88 |
| Llama-3.1-8B (B=4, H=32, Hkv=8, D=128) | bf16 | 81 (wgmma_pipelined) |
**94** | 92 |
| Llama-3.1-70B (B=2, H=64, Hkv=8, D=128) | fp16 | 92 (ws_causal) | 94 |
**196** |
| Llama-3.1-70B (B=2, H=64, Hkv=8, D=128) | bf16 | 86 (wgmma_pipelined)
| 94 | **198** |

**Reading the compute-bound numbers:**
- **fp16 (ws_causal)** reaches **81–90% of FA3** and matches/beats
FlashInfer — TileOPs' strong path.
- **bf16 (wgmma_pipelined)** is markedly weaker (**37–62% of FA3**),
even behind FlashInfer. For the same causal shape fp16 dispatches to
`ws_causal` but bf16 falls back to `wgmma_pipelined`, a separate perf
gap unrelated to this fix (the barrier change has no measurable latency
impact, see above). Worth a follow-up (bf16 support in the persistent
kernel, or better wgmma bf16 autotune).

## Acceptance criteria (tile-ai#1616)

- [x] Both failing fixed-length GQA sliding-window cases pass at
`atol=1e-2, rtol=1e-2`.
- [x] Causal, left-window-only, no-window, and varlen cases remain
green.
- [x] Right-window masking covered by the existing (now-passing)
parametrizations in `test_gqa_sliding_window.py`.

---------

Co-authored-by: Ibuki 🍃 — a wind born from GPTs <Ibuki-wind@users.noreply.github.com>
## Summary

- Canonicalize GQA inference around fixed manifest-backed OP contracts:
- `GroupedQueryAttentionPrefillFwdOp` now owns packed
dense/varlen/sliding-window/FP8 prefill dispatch.
- `GroupedQueryAttentionPrefillPagedWithKVCacheFwdOp` now owns normal
and FP8 paged prefill cache dispatch through fixed scale inputs and
`cache_dtype`.
- `GroupedQueryAttentionDecodePagedWithKVCacheFwdOp` keeps paged decode
as the canonical serving decode path.
- Keep compatibility wrappers where useful, including routing square
`GroupedQueryAttentionFwdOp` through canonical prefill instead of adding
logical Kernel wrappers.
- Keep selector helpers local to `tileops/ops/attention/gqa.py` and
expose concrete kernels through `default_kernel_map` / manifest
`source.kernel_map`.
- Thread `sm_scale` and `softcap` through contiguous and paged GQA
decode, and widen the Hopper WS prefill path to cover bf16 plus
non-default scale/softcap.
- Keep canonical inference outputs fixed as `o`; no dynamic `return_lse`
output arity is introduced.
- Keep backward-only LSE setup out of the production forward op surface:
`GroupedQueryAttentionFwdOp.forward()` returns only `o`, while the
backward workload constructs the real base-2 LSE in its own setup
helper.

Closes tile-ai#1610
Closes tile-ai#1611
Closes tile-ai#1622
Closes tile-ai#1623

## Validation

Local validation:

- `ruff check tileops/ops/attention/gqa.py workloads/attention/gqa.py
tests/ops/attention/test_gqa.py`
- `python -m compileall -q tileops/ops/attention/gqa.py
workloads/attention/gqa.py tests/ops/attention/test_gqa.py`
- `python -m pytest -q tests/ops/attention/test_gqa.py::test_gqa_fwd
tests/ops/attention/test_gqa.py::test_gqa_bwd --tb=short` (`10 passed`)
- `python -m pytest -q tests/ops/attention/test_gqa.py -m smoke
--tb=short` (`58 passed`)
- `python scripts/validate_manifest.py --strict --check-op
GroupedQueryAttentionFwdOp`
- `git diff --check`

Earlier broader GQA validation on this PR branch:

- targeted prefill/paged/decode/FP8 GQA suite passed before the final
output-only forward cleanup;
- GQA manifest strict checks for `GroupedQueryAttentionPrefillFwdOp`,
`GroupedQueryAttentionPrefillPagedWithKVCacheFwdOp`,
`GroupedQueryAttentionDecodeWithKVCacheFwdOp`, and
`GroupedQueryAttentionDecodePagedWithKVCacheFwdOp` passed.

GitHub CI on latest push:

- `pre-commit`: passing
- `validate-manifest`: passing
- `benchmark-contract-tests`: passing
- `gpu-smoke`: passing
Closes tile-ai#1521

## Summary

- Add native grouped Conv2d/Conv3d TileLang kernels for bias and no-bias
variants.
- Wire Conv2d/Conv3d op dispatch to grouped kernels while preserving
existing dilation support from `main`.
- Update the convolution manifest contract for grouped Conv2d/Conv3d
status, kernel maps, and per-group roofline accounting.
- Add grouped correctness coverage and model-derived grouped convolution
benchmark cases.

## Benchmark

```bash
CUDA_VISIBLE_DEVICES=1 TILELANG_CLEANUP_TEMP_FILES=1 python -m pytest \
  'benchmarks/ops/bench_convolution.py::test_conv2d_bench[mobilenetv2-depthwise-fp16]' \
  'benchmarks/ops/bench_convolution.py::test_conv2d_bench[resnext-grouped-3x3-fp16]' \
  'benchmarks/ops/bench_convolution.py::test_conv3d_bench[3d-resnext-grouped-k3-fp16]' \
  'benchmarks/ops/bench_convolution.py::test_conv3d_bench[3d-resnext-grouped-k3-b8-fp16]' \
  -vvs
```

Result: `4 passed in 138.39s`.

| Case | TileOps latency | TileOps TFLOP/s | TileOps bandwidth | Torch
latency | Torch TFLOP/s | Torch bandwidth |
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| `conv2d mobilenetv2-depthwise-fp16` | 0.0064 ms | 0.2812 | 0.0626 TB/s
| 0.0065 ms | 0.2772 | 0.0617 TB/s |
| `conv2d resnext-grouped-3x3-fp16` | 0.0041 ms | 3.4882 | 0.1498 TB/s |
0.0200 ms | 0.7207 | 0.0309 TB/s |
| `conv3d 3d-resnext-grouped-k3-fp16` | 0.0147 ms | 5.8872 | 0.1645 TB/s
| 0.7442 ms | 0.1165 | 0.0033 TB/s |
| `conv3d 3d-resnext-grouped-k3-b8-fp16` | 0.1269 ms | 5.4643 | 0.1519
TB/s | 5.7221 ms | 0.1212 | 0.0034 TB/s |
tile-ai#1634)

> **Hotfix / stopgap.** Unblocks CI and closes a deadlock footgun. Does
**not** fix the root design issue (`block_Q` is exposed as an
independent autotune knob while it is coupled to `heads`); see
Follow-up.

## Problem

`FP8LightingIndexerOp(..., tune=True)` hangs until the per-test timeout
and fails the job. It surfaced on push-to-main / nightly but not on the
introducing PR (tile-ai#1632), because `gpu-smoke` runs `-m smoke` on PRs and
`-m "smoke or full"` only on push — so the `full`-tier `tune=True` node
never ran on that PR's gate.

## Root cause

Mapped on H200 across heads ∈ {32, 64, 128}: **the kernel deadlocks iff
`block_Q == 1` and `num_stages >= 1`** (some warps never reach the
software-pipeline barrier; the launch spins forever). An abandoned hung
launch wedges the GPU, so every later autotune candidate times out too —
the sweep never finishes within budget. Also a non-autotune footgun: the
default `block_Q = 128 // heads` is `1` for `heads >= 65`.

## Fix

- Force `num_stages = 0` when `block_Q < 2` (guards autotune, manual
config, and the default path).
- Harden default to `block_Q = max(1, 128 // heads)`.
- Drop `block_Q == 1` from `autotune_configs`.

Verified on H200: previously-hanging configs now run; `tune=True` test
passes in ~9s warm / ~54s cold (was: timeout).

## Follow-up

Clean fix is to tune the q-tile and *derive* `block_Q` so degenerate
configs can't be expressed (removes the guard). The silent deadlock is
also a tilelang pipeline-codegen robustness bug.
Remove the `cache-warmup` job from `nightly.yml`.

- Delete the `cache-warmup` job.
- `benchmark` becomes the entry job: drop `needs: [cache-warmup]` and
the now-redundant `always() &&`.
- Renumber phase comments (benchmark→1, op_test→2, publish→2b,
packaging→3).
- Keep `scripts/warmup_kernel_cache.py` + `scripts/conftest_warmup.py`
for manual one-off warmup.

Redundant under the persistent `/ci-cache`: the benchmark phase
autotunes at op construction (outside the timed region) and writes
results back, so it self-warms across runs; benchmark numbers are
unchanged. The job never gated anything (warmup step
`continue-on-error`, benchmark `if: always()`) but turned the nightly
red on runner network blips.
…anup (tile-ai#1637)

## Summary

- nightly `op_test` runs `-m "full or nightly"` instead of the whole
suite; the `smoke` tier is already covered by every PR and every
push-to-main gpu-smoke.
- `reclaim-runner-disk` drops the venv-prune pass (venvs retired) and
the `RUNNER_TEMP` orphan-purge pass (ephemeral `--rm` jobs leave no
cross-run orphans), plus their inputs `venv-keep`,
`temp-orphan-minutes`, `current-run-dir`, `prune-tool-cache`.
- `reclaim-runner-disk` keeps only the `/ci-cache` file-trim, atomic
autotuner sentinel-repair/age-trim, and post-reclaim disk floor.
- `gpu-smoke.yml` drops the removed `prune-tool-cache` input;
`runner-maintenance.yml` and `reclaim_cache.sh` are unchanged.

## Test plan

- [x] all workflow/action YAML parses (`yaml.safe_load`)
- [x] `tests/test_reclaim_action.py` 13/13 pass (`reclaim_cache.sh`
untouched)
- [x] pre-commit passed
…ne trace tool (tile-ai#1638)

## Summary

- **Warp-specialized GEMM kernel.** Rewrites the dense `GemmKernel` as a
hand-written two-warpgroup warp-specialized implementation for Hopper
(SM90): a producer warpgroup issues TMA loads into a double-buffered
SMEM ring, a consumer warpgroup runs WGMMA over K. Covers all four
`(trans_a, trans_b)` layouts via the WGMMA transpose flags. fp16/bf16
in, fp32 accumulate. The `GemvKernel` fast path (m==1 / n==1) is
unchanged.
- **In-kernel timeline trace tool (`tileops/trace/`).** Annotate a
kernel body with `trace.group` / `range` / `record` / `dag`;
`trace.out_idx` + `trace.finalize` wire the markers into a vanilla
`@tilelang.jit` builder (lowered when tracing is on, stripped to zero
cost when off); `trace.run` executes the kernel and dumps a
self-contained Plotly HTML timeline. Process-local switch via
`trace.enable()` — no monkeypatch, no env var. `GemmKernel` is
annotated; opt in per pytest run with `--trace-kernel`.

## Motivation

Small-m dense NT GEMM (e.g. 128×2112×7168) was running well under
cuBLAS. The WS kernel targets that regime, and the trace tool provides a
timeline view (kernel execution / gaps / producer–consumer overlap) to
diagnose it.

## Notes / tradeoffs

- **SM90-only.** WS relies on TMA + WGMMA, so
`GemmKernel.supported_archs = [90]`.
- **TMA alignment.** Each input's innermost (contiguous) dimension must
be a multiple of 8 fp16 elements (16-byte). For NT (`A @ Bᵀ`) that is
`K` only; other layouts additionally require the relevant `M` / `N` to
be aligned. Documented on the kernel.
- `_gemm_wrapped_kernel` (torch custom op) is kept for `torch.compile`
compatibility; the eager forward calls the compiled JIT directly (cf.
`GemvKernel`).

## Tests

- All four layouts (NN/NT/TN/TT) verified numerically against torch (max
abs err 0.0), including the `transpose_A` paths.
- Added an NT dense numerics case to `tests/ops/test_gemm.py`; suite
passes (23 passed).

## Follow-ups (not in this PR)

- Unit tests for the `tileops/trace/` package.
- Promote the trace-tool design doc into `docs/design/`.
…tile-ai#1608)

## Summary
- Update default tuning configs for SSD chunk_scan and chunk_state
kernels based on NCU profiling.

## Test plan
- [ ] Run benchmarks/ops/bench_mamba.py for chunk_scan and chunk_state


### ssd_chunk_scan: threads=64 (before) vs threads=128 (after, current
default)

> Config: `{block_l: 64, block_p: 64, block_n: 64, block_s: 64, threads:
64/128}`

| Shape | before (t=64) ms | after (t=128) ms | Speedup |
|---|---|---|---|
| 130m-b1-4k | 0.1137 | 0.0992 | **1.146x** |
| 130m-b8-4k | 0.5349 | 0.4107 | **1.303x** |
| 130m-b4-32k | 1.9326 | 1.4292 | **1.352x** |
| 370m-b1-4k | 0.1337 | 0.1122 | **1.192x** |
| 370m-b8-4k | 0.6749 | 0.5033 | **1.341x** |
| 370m-b4-32k | 2.5624 | 1.8263 | **1.403x** |
| 370m-b32-2k | 1.2850 | 0.9423 | **1.364x** |
| 780m-b1-4k | 0.1778 | 0.1452 | **1.225x** |
| 780m-b8-4k | 0.9909 | 0.7403 | **1.339x** |
| 780m-b4-32k | 3.7741 | 2.7922 | **1.352x** |
| 780m-b16-2k | 0.9901 | 0.7428 | **1.333x** |
| 1p3b-b1-4k | 0.2205 | 0.1733 | **1.273x** |
| 1p3b-b8-4k | 1.2873 | 0.9422 | **1.366x** |
| 1p3b-b2-32k | 2.5459 | 1.8293 | **1.392x** |
| 1p3b-b8-2k | 0.6737 | 0.5040 | **1.337x** |
| 2p7b-b1-4k | 0.2596 | 0.2067 | **1.256x** |
| 2p7b-b4-4k | 0.8391 | 0.6356 | **1.320x** |
| 2p7b-b2-32k | 3.1504 | 2.3596 | **1.335x** |
| 2p7b-b4-2k | 0.4594 | 0.3500 | **1.313x** |

**threads=128 wins on every single production shape, by 15–40%.** The
pre-tile-ai#1608 default of threads=64 is consistently slower. The threads=128
change in this PR is correct and necessary.

---

### ssd_chunk_state: old config (block_n=128, block_p=64, block_l=64,
threads=256) vs new (block_n=min(128,N), block_p=64, block_l=32,
threads=128)

| Shape | before ms | after ms | Speedup |
|---|---|---|---|
| 370m-b1-L2k | 0.0706 | 0.0704 | 1.003x |
| 370m-b4-L2k | 0.0887 | 0.0867 | 1.023x |
| 1p3b-b1-L2k | 0.0707 | 0.0707 | 1.000x |
| 1p3b-b4-L2k | 0.0896 | 0.0852 | 1.051x |
| 2p7b-b1-L2k | 0.0810 | 0.0834 | 0.972x |
| 2p7b-b4-L2k | 0.0837 | 0.0818 | 1.023x |
| 1p3b-b1-L8k | 0.0702 | 0.0675 | 1.039x |

The chunk_state config change is largely neutral (±3%), with a small
regression at 2.7b-b1 (-2.8%). The main benefit from the chunk_state
changes in this PR is the `threads=256 → 128` reduction, which reduces
register pressure. The `block_l=64→32` change has minimal latency impact
at these shapes.
…-ai#1640)

## What

Makes the `tileops.trace` module documentation-ready and adds a usage
tutorial,
so the trace tool renders cleanly on the docs site (mkdocstrings +
Material).

- **Docstrings: Sphinx roles → plain code spans.** Across the whole
`tileops/trace/` package (`__init__`, `api`, `decode`, `markers`,
`passes`,
`record`, `state`, `ui`), replace `:func:` / `:class:` / `:mod:` /
`:data:`
  cross-reference roles with double-backtick code spans. The project is
Google-docstring-only; mkdocstrings does not resolve Sphinx roles, so
they
  rendered as literal `:func:`...`` text on the site.
- **`ui.py` timeline legend fix.** The horizontal legend overlapped the
x-axis
  title. Give the x-axis title a `standoff`, anchor the legend below it
  (`y: -0.30`, `yanchor: top`), and grow the bottom margin (120 → 170).
- **New tutorial: `docs/perf/trace-timeline.md`.** End-to-end
walkthrough —
  write a traced warp-specialized GEMM, run it, enable tracing, read the
timeline. Each code block uses Material code annotations (the `+`
markers)
  linked to the auto-generated API reference.
- **`docs/perf/README.md`**: add a "Tools & Guides" entry pointing at
the tutorial.

## Why

The trace tool shipped in tile-ai#1638 with no rendered API docs or guide; this
makes
its public surface (`tileops.trace.api`) render correctly and gives
users a
single walkthrough.

## Notes

- No behavior change: docstring text and one Plotly layout dict only.
- The site-side wiring (mkdocstrings page, nav, embedded timeline) is a
separate
  PR against `tile-ai/TileOPs.github.io`.

Closes tile-ai#1639
)

## Summary
- Add explicit dense-backend GQA prefill manifest workloads for custom
sm_scale and softcap.
- Thread backend, sm_scale, and softcap from manifest workloads into the
dense prefill benchmark.
- Apply the same sm_scale/softcap settings to the torch reference and
FlashInfer baseline so benchmark comparisons use matching semantics.
- Pass 0.0 instead of None for FlashInfer logits_soft_cap when softcap
is unset.

This PR intentionally updates only manifest/benchmark coverage. The
high-performance kernel scheduling updates will be submitted separately
after this lands.

## Testing
- python -m py_compile benchmarks/ops/attention/bench_gqa.py
benchmarks/ops/attention/manifest_params.py
- python manifest workload expansion check for
GroupedQueryAttentionPrefillFwdOp
- git diff --check
- docker/CI image: pytest
/workspace/TileOPs/benchmarks/ops/attention/bench_gqa.py --collect-only
-q
- docker/CI image: pytest
/workspace/TileOPs/benchmarks/ops/attention/bench_gqa.py -q -k
'test_gqa_prefill_fwd_bench and softcap50'
- docker/CI image: pytest
/workspace/TileOPs/benchmarks/ops/attention/bench_gqa.py -q -k
'test_gqa_prefill_fwd_bench and sm and scale'
- docker/CI image: pytest
/workspace/TileOPs/benchmarks/ops/attention/bench_gqa.py -q -k
'test_gqa_prefill_fwd_bench and llama-3.1-8b-prefill-dense-float16'
## Summary
- Dispatch dense GQA prefill to Hopper warp-specialized kernels for H200
fp16/bf16 causal dim-128 paths.
- Add a square dense prefill fast path that reuses the high-performance
square GQA WS kernel and supports custom `sm_scale` / `softcap`.
- Avoid timed CUDA `torch.equal` checks for explicit dense backend
inputs by validating fixed packed lengths directly.
- Make paged prefill forward kernels output-only for inference paths and
keep RoPE / FP8-cache / softcap variants on the canonical paged OP.
- Add focused tests for dense dispatch, feature variants, and dense
backend packed-input validation.

This follows tile-ai#1641, which added the manifest/benchmark coverage for
scale and softcap cases.

## Performance Notes
H200, torch 2.10.0+cu129, CUDA 12.9, driver 575.57.08.

Dense prefill benchmark (`test_gqa_prefill_fwd_bench`):
- B4 S512 H32/Hkv8 D128 fp16: TileOps 0.0394 ms vs FlashInfer 0.0397 ms
- B4 S512 H32/Hkv8 D128 bf16: TileOps 0.0393 ms vs FlashInfer 0.0398 ms
- B2 Sq512 Skv4096 H32/Hkv8 D128 fp16: TileOps 0.1247 ms vs FlashInfer
0.1266 ms
- B2 Sq512 Skv4096 H32/Hkv8 D128 bf16: TileOps 0.1240 ms vs FlashInfer
0.1258 ms
- B1 Sq512 Skv4096 H64/Hkv8 D128 fp16: TileOps 0.1233 ms vs FlashInfer
0.1245 ms
- B1 Sq512 Skv4096 H64/Hkv8 D128 bf16: TileOps 0.1223 ms vs FlashInfer
0.1239 ms

Focused feature benchmarks:
- sm_scale=0.125: TileOps 0.0391 ms vs FlashInfer 0.0395 ms
- softcap=50.0: TileOps 0.0455 ms vs FlashInfer 0.0459 ms

Paged representative benchmarks:
- Llama3.1 paged RoPE: 1.9863 ms
- paged softcap: 0.1759 ms
- Llama3.1 FP8 KV cache: 2.0418 ms
- FP8 KV cache softcap: 0.2440 ms

## Testing
- `python -m py_compile tileops/kernels/attention/gqa_prefill_fwd_ws.py
tileops/kernels/attention/gqa_fwd_ws.py
tileops/kernels/attention/gqa_fwd.py tileops/ops/attention/gqa.py
tests/ops/attention/test_gqa.py`
- `git diff --check`
- docker/CI image: `pytest
/workspace/TileOPs/tests/ops/attention/test_gqa.py -q -k 'prefill_fwd'`
-> 19 passed
- docker/CI image: `pytest
/workspace/TileOPs/tests/ops/attention/test_gqa_prefill_paged.py -q` ->
27 passed
- docker/CI image: `pytest
/workspace/TileOPs/benchmarks/ops/attention/bench_gqa.py --collect-only
-q` -> 34 collected
- docker/CI image: `python scripts/validate_manifest.py --levels
schema,signature,shape,dtype,bench --strict`
- docker/CI image: `python -m pytest -q benchmarks/tests` -> 8 passed
- docker/CI image: `pytest
/workspace/TileOPs/benchmarks/ops/attention/bench_gqa.py -q -k
'test_gqa_prefill_fwd_bench'` -> 8 passed
- docker/CI image: `pytest
/workspace/TileOPs/benchmarks/ops/attention/bench_gqa.py -q -k
'test_gqa_prefill_fwd_bench and sm and scale'` -> 1 passed
- docker/CI image: `pytest
/workspace/TileOPs/benchmarks/ops/attention/bench_gqa.py -q -k
'test_gqa_prefill_fwd_bench and softcap50'` -> 1 passed
- docker/CI image: `pytest
/workspace/TileOPs/benchmarks/ops/attention/bench_gqa.py -q -k
'test_gqa_prefill_paged_with_kv_cache_fwd_bench and (llama31 or
softcap50)'` -> 4 passed
## Summary

- fix `MHCPostKernel` shared-memory indexing to use `block_C` as the row
stride for `x_out_shared`
- avoid overlapping writes across `n_expand` rows in the default
`n_expand=4, block_C=64` configuration

Fixes tile-ai#1584.

## Tests

- `python -m py_compile tileops/kernels/mhc/mhc_post.py
tileops/ops/mhc.py tests/ops/test_mhc_post.py`
- `python -m ruff check tileops/kernels/mhc/mhc_post.py
tests/ops/test_mhc_post.py`
- Docker runner: `python -m pytest tests/ops/test_mhc_post.py -vvs -k
smoke`
- Docker runner: `compute-sanitizer --tool racecheck python -m pytest
tests/ops/test_mhc_post.py -q -k smoke`
  - `RACECHECK SUMMARY: 0 hazards displayed (0 errors, 0 warnings)`
- Docker runner: `python -m pytest tests/ops/test_mhc_post.py -q`

Host pytest was not used for validation because the host environment is
missing `tvm.tirx`; the Docker runner image includes the TileOps CI
dependencies.
…ile-ai#1645)

Closes tile-ai#1644

## Summary

This PR addresses the poor `conv2d` performance reported in tile-ai#1644 (CI
run `28398916051`) by introducing `Conv2dSymmetricKernel`, an
NHWC-implicit-GEMM path for symmetric conv2d cases (`kernel_h ==
kernel_w`, `groups == 1`, `c_in % 32 == 0`).

Key changes:
- Added `Conv2dSymmetricKernel` and dispatched symmetric conv2d cases to
it.
- Unified the three layout transpose macros (`nchw_to_nhwc_input`,
`kcrs_to_krsc_weight`, `nhwc_to_nchw_output`) into a single
`transpose_spatial_channel` macro.
- Parameterized tile size and fastest-varying dimension so the transpose
kernels can be tuned per shape in follow-up work.
- Reduced the weight transpose `spatial_block` from `32` to `16`, which
increases the spatial grid for small kernels (`3x3`, `5x5`, `7x7`).
- Added compile-time asserts for tile/thread constraints.

No public API is modified.

## Test plan

- [x] `python -m py_compile tileops/kernels/convolution.py` passed
- [x] `tests/ops/test_convolution.py -k "test_conv2d"` — 20 passed
- [x] `tests/ops/test_convolution.py -k "dispatches"` — 6 passed

## Benchmark

Comparison of this branch (`Conv2dSymmetricKernel`) against
`upstream/main` (`Conv2dKernel`) on H200, clock-locked 1500 MHz:

| case | main Conv2dKernel (ms) | this branch Conv2dSymmetricKernel (ms)
| speedup |

|------|------------------------|----------------------------------------|---------|
| deeplabv3-aspp-3x3-rate12-fp16 | 0.4278 | 0.1152 | **3.71x** |
| stage-transition-5x5-s2-fp16 | 0.0748 | 0.0242 | **3.09x** |
| midres-5x5-s1-fp16 | 0.0392 | 0.0183 | **2.14x** |
| stride2-bf16 | 0.0279 | 0.0136 | **2.05x** |
| stage-transition-3x3-s2-fp16 | 0.0279 | 0.0163 | **1.71x** |
| resnet-3x3-fp16 | 0.0169 | 0.0150 | **1.13x** |

`Conv2dSymmetricKernel` is faster on all representative symmetric cases,
with larger gains on bigger/dilated kernels. Further improvements are
expected once the new transpose tile parameters are exposed to autotune.

## Additional context

Addressed Gemini Code Assist review feedback:
- Renamed local `h`/`w` to `h_idx`/`w_idx` in
`transpose_spatial_channel` to avoid shadowing outer parameters.
- Switched to interleaved channel mapping for coalesced NHWC writes in
the `channel_fastest` branch.
- Added `c_in % block_k == 0` filtering in
`Conv2dSymmetricKernel.autotune_configs`.
- Made bias addition conditional on `has_bias` in
`conv_nhwc_implicit_gemm_bias`.

This change is the first step toward a larger performance fix for tile-ai#1644.
The unified macro makes it straightforward to add `input_spatial_block`,
`output_spatial_block`, etc. to `Conv2dSymmetricKernel.autotune_configs`
in a follow-up PR.
…1647)

## Summary
- keep public packed `backend="dense"` and `backend="fp8"` GQA prefill
safe by validating uniform `cu_seqlens` by default before reshaping to
BSHD
- add an explicit public `validate_uniform_cu_seqlens` switch so
high-performance callers with known-uniform packed metadata can skip the
CUDA value check without using a separate benchmark-only path
- keep `backend="auto"` requiring uniform validation because auto
dispatch needs the `cu_seqlens` values to choose dense vs varlen
- declare `validate_uniform_cu_seqlens` in the op manifest and mark
known-uniform dense/fp8 manifest workloads with
`validate_uniform_cu_seqlens=False`
- update dense and fp8 prefill benchmarks to consume the manifest
setting and call the same public op path users call
- add tests covering default dense/fp8 ragged rejection, explicit dense
validation skip, auto validation requirements, varlen-style skip
behavior, and the BSHD wrapper path

This addresses the dense packed-input contract concern from review while
keeping benchmark and user calls on the same public API.

## Testing
- `python -m ruff check tileops/ops/attention/gqa.py
tests/ops/attention/test_gqa.py benchmarks/ops/attention/bench_gqa.py
benchmarks/ops/attention/bench_gqa_fp8.py
benchmarks/ops/attention/manifest_params.py`
- `python -m py_compile tileops/ops/attention/gqa.py
tests/ops/attention/test_gqa.py benchmarks/ops/attention/bench_gqa.py
benchmarks/ops/attention/bench_gqa_fp8.py
benchmarks/ops/attention/manifest_params.py`
- `git diff --check`
- Docker (`ghcr.io/tile-ai/tileops-runner:65dbc98-torch2.10`): `python
scripts/validate_manifest.py --check-op
GroupedQueryAttentionPrefillFwdOp --levels
schema,signature,shape,dtype,bench --strict`
- Docker (`ghcr.io/tile-ai/tileops-runner:65dbc98-torch2.10`): `python
-m pytest tests/ops/attention/test_gqa.py -q -k
"explicit_varlen_backends or explicit_dense_backends or
explicit_dense_can_skip or auto_backend_requires or bshd_wrapper or
auto_backend_checks"`
- Docker as root for writable TileLang cache/benchmark log: `python -m
pytest
benchmarks/ops/attention/bench_gqa.py::test_gqa_prefill_fwd_bench[llama-3.1-8b-prefill-dense-float16]
-q -s`

Host pytest was not used for validation because the host environment is
missing `tvm.tirx`; the Docker runner image includes the TileOps CI
dependencies.
## Summary

Closes tile-ai#1648.

This updates the initial tile-ai#1648 scope so derivable shape metadata is
inferred from op inputs instead of being required as constructor
arguments:

- Norm family: `FusedAddLayerNormFwdOp`, `FusedAddRMSNormFwdOp`,
`AdaLayerNormFwdOp`, `AdaLayerNormZeroFwdOp`
- Scan family: `CumsumFwdOp`, `CumprodFwdOp`
- MoE leaf ops: `FusedTopKOp`, `MoePermuteNopadFwdOp`

The old explicit constructor arguments remain supported as
compatibility/strict-validation commitments, while the preferred API
derives `M`, `N`, token counts, top-k shape, hidden size, and dtype from
the input tensors at `forward()` time. Kernels are built lazily and
cached by the inferred specialization.

## Manifest / callers

- Removed input-derivable `static_dims` from the affected norm and scan
manifest entries.
- Updated `MoePermuteNopadFwdOp` manifest params/workloads/roofline to
treat `hidden_states` and `topk_ids` shapes as the source of truth,
keeping `num_experts` explicit.
- Updated direct benchmarks and composite call sites to use the
preferred input-inferred APIs.
- Added compatibility and cache-reuse/changing-shape test coverage.

## Testing

Using the TileOpsGov runner image
`ghcr.io/tile-ai/tileops-runner:65dbc98-torch2.10` on H200 GPU:

- `python -m pytest -q tests/ops/test_cumulative.py
tests/ops/test_fused_add_layer_norm.py
tests/ops/test_fused_add_rms_norm.py tests/ops/test_ada_layer_norm.py
tests/ops/test_ada_layer_norm_zero.py tests/ops/test_moe_fused_topk.py
tests/ops/test_moe_permute_nopad.py -m smoke --tb=short --timeout=900
--timeout-method=thread`
  - `67 passed, 52 deselected, 14 warnings`
- `python -m pytest -q tests/ops/test_cumulative.py
tests/ops/test_fused_add_layer_norm.py
tests/ops/test_fused_add_rms_norm.py tests/ops/test_ada_layer_norm.py
tests/ops/test_ada_layer_norm_zero.py tests/ops/test_moe_fused_topk.py
tests/ops/test_moe_permute_nopad.py --tb=short --timeout=900
--timeout-method=thread`
  - `119 passed, 14 warnings`
- `python scripts/validate_manifest.py`
  - `All manifest checks passed`

Note: the validator reports an advisory warning that
`MoePermuteNopadFwdOp.num_experts` has an `__init__` default of `None`
while the manifest has no default. This is intentional to preserve old
positional construction compatibility; `forward()` still requires
`num_experts` to be provided.
…ads (tile-ai#1652)

## Summary

Adds a warp-specialized (Hopper) batch=1 GQA decode kernel and routes
the
`GroupedQueryAttentionDecodeWithKVCacheFwdOp` to it when the fast-path
preconditions hold. Migrated from a validated PoC and hardened for
production
(runtime `real_seqlen_kv` masking + split redistribution).

Includes the batch=1 workloads and a proper FlashInfer baseline needed
to
benchmark it.

## What's in it

**Workloads + benchmark baseline** (`manifest`, `bench_gqa_decode.py`)
- Eight batch=1 Qwen3-30B-A3B decode workloads (KV 1K–256K).
- FlashInfer baseline now uses `single_decode_with_kv_cache` (contiguous
KV,
no paging overhead) for batch=1 — the apples-to-apples comparison; the
paged
  batched wrapper is kept for batch>1.

**Kernel + dispatch** (`gqa_decode_bs1.py`, `gqa.py`, tests,
`kernel_map`)
- `GQADecodeBs1Kernel` (`supported_archs=[90]`): TMA + mbarrier + wgmma
warp-specialized producer/consumer, online softmax in exp2 domain, fp32
  accumulators, ctx-split + LSE-combine.
- Op-level ctor gate selects the fast-path kernel only for batch=1,
Hopper,
fp16, D=128, softcap=0, group in [1,64]; everything else keeps the
incumbent
  `GQADecodeKernel`. sm80/89 stay constructible.
- `forward()` dispatches by runtime `real_seqlen_kv`: `no_split` below
1024,
ctx-split above (largest of {32,16,8} that divides `real`, balanced
load).
Non-aligned lengths (e.g. 2049) go through this kernel, not a fallback.

## Correctness

14 tests pass (`tests/ops/attention/test_gqa_decode.py`), including
runtime
context-switch and non-divisible-length cases; atol=rtol=1e-2 vs the
fp32
reference. Hopper-only tests skip on other arches.

## Performance (H200 @ 1500MHz, CUPTI, H=32 Hkv=4 D=128, fp16)

| KV len | new (µs) | incumbent | FlashInfer single | new/incumbent |
new/FI |

|-------:|---------:|----------:|------------------:|--------------:|-------:|
| 1024 | 7.97 | 16.16 | 10.35 | 0.49 | 0.77 |
| 2048 | 8.94 | 10.65 | 11.12 | 0.84 | 0.80 |
| 3072 | 11.93 | 24.07 | 11.86 | 0.50 | 1.01 |
| 4096 | 10.70 | 12.78 | 12.40 | 0.84 | 0.86 |
| 8192 | 14.00 | 16.20 | 14.81 | 0.86 | 0.95 |
| 32768 | 29.49 | 36.52 | 35.29 | 0.81 | 0.84 |
| 131072 | 77.53 | 119.36 | 83.88 | 0.65 | 0.92 |

Faster than the incumbent across the board; faster than or on par with
FlashInfer `single_decode` everywhere (roughly tied at 3072).

## Trust-model note

Kept as a single PR per request. It mixes a manifest workloads change
(normally
a separate human-reviewed PR) with the implementation; the manifest
edits are
the eight workloads plus one `source.kernel_map` line (the latter is
within the
implementation-PR carve-out).
## Summary

Closes tile-ai#1659. Refs tile-ai#1650.

Promotes implemented special elementwise ops from `spec-only` to
`implemented`:

- `SiluAndMulFwdOp`, `GeluAndMulFwdOp`, `GeluTanhAndMulFwdOp`
- `AlibiFwdOp`, `SinusoidalFwdOp`

This is a code/manifest cleanup PR, not a release-plan doc update. tile-ai#1659
is tracked as a sub-issue of the broader tile-ai#1650 release-facing manifest
coverage tracker.

## Changes

- Allow fused gated ops to bind `M`, `N`, and `dtype` from the runtime
input tensor, while preserving the old explicit ctor path.
- Keep lazy fused gated ops reusable across runtime shapes/dtypes by
separating explicit ctor contracts from the last runtime-bound kernel
specialization.
- Ensure `_eager_forward` validates the runtime input and calls
`_ensure_kernel(M, N, x.dtype)` before launch.
- Add fused gated `eval_roofline()` support and the correct GELU-tanh
FLOPs coefficient.
- Add zero-input `_validate_dtypes()`, `_infer_output_shapes()`,
`total_memory`, and `eval_roofline()` for `AlibiFwdOp` and
`SinusoidalFwdOp`.
- Keep generative op `dtype` explicit in the manifest because these ops
have no input tensor to infer dtype from.
- Add smoke tests for manifest-style fused gated lazy binding and shape
rebinding.

## Validation

Ran in `ghcr.io/tile-ai/tileops-runner:65dbc98-torch2.10`:

```bash
python -m pytest tests/test_ops_manifest.py -q
# 21 passed

python -m pytest tests/test_ops_manifest.py::TestOpSchema::test_workloads_include_all_required_params -q
# 1 passed

for op in SiluAndMulFwdOp GeluAndMulFwdOp GeluTanhAndMulFwdOp AlibiFwdOp SinusoidalFwdOp; do
  python scripts/validate_manifest.py --check-op "$op" --strict
done
# all targeted checks passed

python -m pytest \
  tests/ops/test_fused_gated.py::test_silu_and_mul_infers_manifest_shape_contract \
  tests/ops/test_fused_gated.py::test_silu_and_mul_lazy_op_rebinds_shape \
  tests/ops/test_fused_gated.py::test_fused_gated_rejects_integer_dtype -q
# 3 passed

python -m ruff check \
  tileops/ops/elementwise/_base.py \
  tileops/ops/elementwise/fused_gated.py \
  tileops/ops/elementwise/alibi.py \
  tileops/ops/elementwise/sinusoidal.py \
  tests/ops/test_fused_gated.py
# All checks passed
```

GitHub CI is green, including `validate-manifest`,
`benchmark-contract-tests`, and `gpu-smoke`.
…ock factorization (tile-ai#1646)

## Summary

This PR optimizes the Mamba-2 SSD chunk scan forward kernel with two key
improvements:
1. **Auto-pipeline optimization**: Leveraging TileLang's built-in
auto-pipeline for better instruction overlap
2. **Micro-block factorization**: Reducing exponential operations in
diagonal blocks via factorized computation

**Performance**: ~4-5% speedup on representative workloads while
maintaining numerical correctness.

## Motivation

The SSD chunk scan kernel is compute-bound on the diagonal path
exponential operations. The original implementation had two bottlenecks:

1. **Manual pipelining limitations**: Hand-written double-buffering
didn't use async copies, limiting memory-compute overlap
2. **Redundant exponentials**: Each element in diagonal blocks computed
`exp(dA[l] - dA[s])` independently, with ~6% theoretical overhead

## Changes

### 1. Auto-Pipeline Optimization

**Before**: Manual double-buffer implementation without async memory
operations
```python
# Manual pipeline with synchronous loads
for n_blk in range(T.ceildiv(N, block_n)):
    # Load, compute, store in lock-step
```

**After**: TileLang auto-pipeline with optimal staging
```python
for n_blk in T.Pipelined(T.ceildiv(N, block_n), num_stages=3):
    # Auto-managed async loads with optimal overlap
```

**Impact**: 
- Optimal `num_stages=3` determined via benchmarking (tested 1, 3, 5)
- Better overlap of global memory loads with tensor core operations
- Cleaner code with equivalent or better performance

### 2. Micro-block Factorization for Diagonal Path

**Optimization**: For diagonal blocks where `s` and `l` are close,
factorize the exponential:

```
exp(dA[l] - dA[s]) = exp(dA[l] - anchor) * exp(anchor - dA[s])
```

where `anchor = dA[l_block_start]` for each 32×32 micro-block.

**Before**: 64×64 direct computation
```python
for ll, ss in T.Parallel(64, 64):  # 4096 exp calls
    exp_factor = T.exp(dA[l] - dA[s])
    lcb_cast[ll, ss] = cb[ll, ss] * exp_factor * dt[ss]
```

**After**: 2×2 micro-block factorization (for block_l=block_s=64)
```python
# Precompute factorized terms (64 + 2*64 = 192 exp calls)
for ll in T.Parallel(64):
    exp_l_micro[ll] = T.exp(dA[ll] - anchor[r])
for r, ss in T.Parallel(2, 64):
    exp_s_micro[r, ss] = T.exp(anchor[r] - dA[ss]) * dt[ss]

# Four 32×32 micro-blocks:
# - Diagonal blocks (mr=0,mc=0 and mr=1,mc=1): direct exp (2*1024 = 2048)
# - Lower block (mr=1,mc=0): factorized (1024, reuses precomputed)
# - Upper block (mr=1,mc=1): zeros (masked out by causality)
```

**Exponential count reduction**:
- Original: 4096 exp operations per 64×64 block
- Optimized: 192 (precompute) + 2048 (diagonal micro-blocks) = 2240 exp
operations
- **Reduction: ~45% fewer exp calls in diagonal blocks**

**Implementation note**: Currently uses Python static unrolling for the
2×2 decomposition, limiting applicability to `block_l=block_s=64`
configuration. Future work could generalize this with dynamic tiling.

## Numerical Correctness

Micro-block factorization introduces two-step rounding but maintains
acceptable numerical accuracy:

**Error Analysis**:
- Mean absolute error: 1.97e-06 (negligible)
- Max absolute error: 7.75e-02 (rare outliers, 0.0003% of elements)
- All outputs pass `torch.allclose(rtol=1e-3, atol=1e-3)` against
reference

Error sources:
1. Two-stage rounding: `exp(a-b)` vs `exp(a-c) * exp(c-b)` 
2. Both mathematically equivalent but different floating-point paths
3. Verified no data races; errors are deterministic from FP arithmetic

The error is acceptable for ML workloads and significantly below typical
FP16 training noise.

## Testing

- ✅ All existing unit tests pass (`test_ssd_chunk_scan_fwd`)
- ✅ Numerical correctness verified against PyTorch reference
- ✅ Tested on smoke and full test configurations
- ✅ No regressions on non-optimized code paths


## Three-Way Comparison

| Config | TileOPs-OLD (ms) | TileOPs-NEW (ms) | mamba_ssm (ms) | NEW vs
OLD | NEW vs mamba_ssm |

|--------|------------------|------------------|----------------|------------|------------------|
| latency-130m-4k | 0.0951 | 0.0930 | 0.1420 | **1.02x faster** ✅ |
**1.53x faster** ✅ |
| serving-130m-4k | 0.3991 | 0.3403 | 0.4395 | **1.17x faster** ✅ |
**1.29x faster** ✅ |
| longctx-130m-32k | 0.7445 | 0.6456 | 0.7952 | **1.15x faster** ✅ |
**1.23x faster** ✅ |
| latency-370m-4k | 0.1692 | 0.1465 | 0.2106 | **1.15x faster** ✅ |
**1.44x faster** ✅ |
| serving-370m-4k | 0.9359 | 0.7946 | 1.0044 | **1.18x faster** ✅ |
**1.26x faster** ✅ |
| longctx-370m-32k | 1.8240 | 1.5885 | 1.9419 | **1.15x faster** ✅ |
**1.22x faster** ✅ |
| throughput-370m-2k | 1.8128 | 1.5905 | 1.9422 | **1.14x faster** ✅ |
**1.22x faster** ✅ |
| latency-2p7b-4k | 0.2012 | 0.1755 | 0.2466 | **1.15x faster** ✅ |
**1.41x faster** ✅ |
| serving-2p7b-4k | 1.2003 | 1.0108 | 1.2210 | **1.19x faster** ✅ |
**1.21x faster** ✅ |


## Future Work

1. **Generalize micro-block size**: Make factorization work for
arbitrary block sizes, not just 64×64
2. **Auto-tuning**: Dynamically select micro-block size based on problem
dimensions
3. **Larger workloads**: Test on production-scale sequence lengths
(4K-32K tokens)

## References

- Original implementation: `ssd_chunk_scan_before_microtile.py`
- Mamba-2 paper: https://arxiv.org/abs/2405.21060
- TileLang auto-pipeline: https://github.com/TileOPs/TileLang

---

**Checklist**:
- [x] Code follows project style guidelines
- [x] Tests pass
- [x] Performance benchmarks included
- [x] Numerical accuracy verified
- [x] Documentation updated
RuneFang and others added 6 commits July 7, 2026 14:43
## Summary

Adds a new performance profile for the **NVIDIA H20-3e** (SXM, 141GB
HBM3e) at `tileops/perf/profiles/h20_3e.yaml`, following the same schema
as the existing `h200.yaml`.



## What's included

| Metric | Theoretical | Calibration |
|---|---|---|
| HBM bandwidth | 4.80 TB/s | **0.8083** | 
| Tensor Core FP16 (dense) | 148 TFLOPS | **0.9500** | 
| Tensor Core BF16 (dense) | 148 TFLOPS | **0.9500** | 
| Tensor Core FP8 (dense)  | 296 TFLOPS | **0.9500** | 
| Tensor Core TF32 (dense) | 74 TFLOPS  | 0.75 *(placeholder)* | 

## Why calibration ratios differ from `h200.yaml`

The H20 is an export-compliance SKU: NVIDIA **policy-caps** its
advertised compute throughput to roughly 15% of H100, while the
underlying silicon (and HBM3e stack) is essentially identical to
H100/H200. Consequently:

- **Tensor Core ~0.95** (vs. H200 ~0.75): the compute cap is the binding
limit, so real GEMMs saturate the throttled peak almost entirely.
- **HBM ~0.81** (vs. H200 ~0.85): the HBM3e stack is physically the same
as H200's; the small delta reflects the lower advertised peak, not a
memory-subsystem difference.
## Summary

Promote existing code-present operators into the release-facing manifest
surface for the bounded tile-ai#1660 batch, excluding Linear Attention and
Mamba2/SSD work.

This PR adds manifest coverage for:

- RoPE variants, including `RopeNeoxPositionIdsOp`
- `DropoutOp`
- narrow FP8 helper surfaces: `FP8QuantOp`, `FP8LightingIndexerOp`
- `TopkSelectorOp`
- standalone `GroupedGemmOp`
- Engram fwd/bwd/decode
- `FFTC2COp`
- `MHCPreOp` / `MHCPostOp`

It also adds matching roofline formulas for these promoted entries.

## Scope notes

This PR intentionally keeps the current runtime surface as the source of
truth and avoids broad API redesigns. In particular:

- Linear Attention and Mamba2/SSD promotion are left out of this batch.
- New low-precision compute-core contracts such as FP8 GEMM,
block-scaled GEMM, low-bit GEMM, sparse GEMM, and special swizzled
layouts are out of scope.
- `_infer_output_shapes` coverage and manifest-driven benchmark
migration are kept as follow-up work. Existing validator warnings in
those areas are advisory and do not block `validate_manifest.py
--strict`.
- Existing benchmark files are referenced with `bench_manifest_driven:
false` to represent the current coverage honestly.

## Validation

Ran in the TileOpsGov CI Docker image:

```bash
docker run --rm --entrypoint /bin/bash --gpus all --ipc host \
  -e PYTHONDONTWRITEBYTECODE=1 \
  -e PYTHONPYCACHEPREFIX=/tmp/pycache \
  -e RUFF_CACHE_DIR=/tmp/ruff-cache \
  -v /home/ga/TileOPs-issue1650-wave0:/workspace \
  -w /workspace \
  ghcr.io/tile-ai/tileops-runner:65dbc98-torch2.10 \
  -lc 'python scripts/validate_manifest.py --strict'
```

```bash
docker run --rm --entrypoint /bin/bash --gpus all --ipc host \
  -e PYTHONDONTWRITEBYTECODE=1 \
  -e PYTHONPYCACHEPREFIX=/tmp/pycache \
  -e RUFF_CACHE_DIR=/tmp/ruff-cache \
  -v /home/ga/TileOPs-issue1650-wave0:/workspace \
  -w /workspace \
  ghcr.io/tile-ai/tileops-runner:65dbc98-torch2.10 \
  -lc 'python -m ruff check tileops/perf/formulas.py'
```

Both passed. `validate_manifest.py --strict` reports existing advisory
warnings for shape-infer rollout and non-manifest-driven benches, but no
hard errors.

Closes tile-ai#1660.
Part of tile-ai#1650.
Closes tile-ai#1654

## Summary

- Add an `AvgPool2dSpatialKernel` fast path for the common NCHW case
with `ceil_mode=False`, `count_include_pad=True`, and no
`divisor_override`.
- Rewrite the general `AvgPool2dKernel` fallback to use a flat
`N*C*H_out*W_out` launch grid and remove the `block_c` tuning dimension.
- Extend `AvgPool2dFixture` coverage for `ceil_mode`,
`count_include_pad`, and `divisor_override` combinations.

## Test plan

- [x] Cleared TileLang caches before the final benchmark/profiling run:

```bash
rm -rf /home/lyc/.tilelang \
  /home/lyc/Project/TileOps-workspace2/.tmp/tilelang_clear_sync_cache \
  /home/lyc/Project/TileOps-workspace2/.tmp/tilelang_ifelse_cache
```

- [x] Functional tests:

```bash
CUDA_VISIBLE_DEVICES=1 conda run -n tileops-dev \
  python -m pytest tests/ops/test_pool.py -k avg_pool2d -vvs
```

Result:

```text
23 passed, 28 deselected, 14 warnings in 57.08s
```

- [x] Benchmark:

```bash
CUDA_VISIBLE_DEVICES=1 conda run -n tileops-dev \
  python -m pytest benchmarks/ops/bench_pool.py::test_avg_pool2d_bench -vvs
```

Result:

```text
3 passed, 15 warnings in 34.03s
```

- [x] Whitespace check:

```bash
git diff --check
```

## Benchmark

Final benchmark on NVIDIA H200:

| workload | kernel path | before TileOps ms | after TileOps ms |
torch-ref ms | after vs before | after vs torch |
| --- | --- | ---: | ---: | ---: | ---: | ---: |
| `vision-3x3-s2` | `AvgPool2dSpatialKernel` | `0.0505` | `0.0046` |
`0.0094` | `10.98x faster` | `2.04x faster` |
| `vision-5x5-s2` | `AvgPool2dSpatialKernel` | `0.0372` | `0.0045` |
`0.0102` | `8.27x faster` | `2.27x faster` |
| `ceil-divisor-bf16` | `AvgPool2dKernel` flat general | `0.0149` |
`0.0038` | `0.0080` | `3.92x faster` | `2.11x faster` |

Autotune best configs from the final run:

| workload | kernel | best config |
| --- | --- | --- |
| `vision-3x3-s2` | `AvgPool2dSpatialKernel` | `{"block_m": 256,
"threads": 256}` |
| `vision-5x5-s2` | `AvgPool2dSpatialKernel` | `{"block_m": 512,
"threads": 256}` |
| `ceil-divisor-bf16` | `AvgPool2dKernel` | `{"block_m": 256, "threads":
256}` |

Notes on the baseline values:

- The `vision-3x3-s2` and `vision-5x5-s2` "before" values are from the
original TileOps kernel measured during the avg_pool2d performance
investigation.
- The `ceil-divisor-bf16` "before" value is the local pre-flat-general
fallback re-run used for the review comparison.

### Profiling summary

The benchmark improvement is consistent with fixed-config `nsys` and
`ncu` data.

For `vision-3x3-s2`, `ncu` shows that TileOps executes much less work
than torch:

| metric | TileOps | torch |
| --- | ---: | ---: |
| Duration | `5.12 us` | `10.05 us` |
| Waves/SM | `1.48` | `1.48` |
| Achieved Occupancy | `73.61%` | `79.44%` |
| SM Busy | `44.53%` | `67.92%` |
| Memory Throughput | `628.30 GB/s` | `321.45 GB/s` |
| Issued Instructions | `1.09M` | `4.24M` |

Torch has higher SM busy, but it issues about `3.9x` more instructions.
The TileOps fast path avoids the general pooling divisor/control path
and reaches about `2x` higher effective memory throughput.

For `vision-5x5-s2`, fixed-config `nsys` reports:

| metric | TileOps | torch |
| --- | ---: | ---: |
| nsys median | `3.808 us` | `9.664 us` |
| nsys avg | `3.822 us` | `9.672 us` |
| Grid / Block | `392 x 256` | `196 x 1024` |
| Registers/thread | `29` | `32` |

For `ceil-divisor-bf16`, the new flat general fallback is also faster
than torch:

| metric | TileOps | torch |
| --- | ---: | ---: |
| nsys median | `3.456 us` | `7.488 us` |
| ncu Duration | `4.48 us` | `8.96 us` |
| Grid / Block | `914 x 256` | `229 x 1024` |
| Waves/SM | `0.87` | `0.87` |
| Achieved Occupancy | `68.44%` | `73.10%` |
| SM Busy | `43.02%` | `65.50%` |
| Eligible Warps/Scheduler | `1.42` | `3.65` |
| Memory Throughput | `404.46 GB/s` | `203.60 GB/s` |
| Issued Instructions | `875K` | `3.28M` |

Torch again keeps the SMs busier, but it issues about `3.75x` more
instructions. The flat general kernel removes the old `block_c` split,
launches over `N*C*H_out*W_out`, and completes the same workload with a
shorter instruction path and higher effective memory throughput.
…1596)

Closes tile-ai#1595.

## Summary

Adds `GatedDeltaNetPrefillFwdOp`, an optimized zero-state inference
prefill operator for Gated DeltaNet. The op returns `(o, final_state)`
without training-only intermediates and provides a TileOps-owned serving
path for long-context prefill.

## Implementation

- Adds TileLang kernels for the BTHD serving path: chunk cumsum,
blocked/Neumann-style A producer, CP initial-state correction, and fused
replay/output.
- Adds shape-aware dispatch for the primary serving contract:
`layout="bthd"`, `B=1`, `fp16`, `DK=DV=128`, `chunk=64`, with an
environment kill switch.
- Keeps API compatibility for BTHD plus head-major BHTD/BHSD aliases.
- Adds op registration, tests, benchmark coverage, manifest workload,
and roofline formula support.
- Fixes the manifest roofline path to decode BTHD workload shapes as
`[B, T, H, D]` instead of `[B, H, T, D]`.

## Third-party Attribution

This PR also adds `THIRD_PARTY_NOTICES.md` and clarifies source-file
headers for the GDN prefill path. Qwen FlashQLA is the MIT-licensed
schedule-level reference for the h-state / corrected-segment-start
CP-split scheduling. The local utility file is adapted from FlashQLA's
utility source and keeps its upstream copyright notice. TileOps-specific
work includes the public op wrapper, BTHD/head-major API integration,
shape-aware dispatch, blocked/Neumann-style A producer integration,
local replay/output implementation, TileLang compatibility changes,
manifest/roofline support, tests, benchmarks, and verification.

## Shape Contract

Primary BTHD path:

- `q, k`: `[B, T, H, DK]`
- `v, o`: `[B, T, H, DV]`
- `g, beta`: `[B, T, H]`
- `initial_state, final_state`: `[B, H, DK, DV]`

Head-major BHTD/BHSD layouts remain supported through the public op API,
but the optimized long-context serving dispatch targets BTHD.

## Performance Headline

H200/GPU4, BTHD, fp16, `B=1`, `DK=DV=128`, `chunk=64`, TileOps benchmark
timer/CUPTI path. FlashQLA is reported as a public TL0.1.8 anchor and
should be read as a public-environment comparison, not a same-lowering
attribution experiment. Qwen FlashQLA is credited as the reference for
the CP-split schedule family.

| Shape | TileOps | FLA | FlashQLA | TileOps / FLA | TileOps / FlashQLA
|
| --- | ---: | ---: | ---: | ---: | ---: |
| 32K / H16 | 0.369577 ms | 2.364970 ms | 0.583178 ms | 6.399x | 1.578x
|
| 64K / H16 | 0.690859 ms | 5.265457 ms | 1.331969 ms | 7.622x | 1.928x
|
| 128K / H16 | 1.227298 ms | 10.899666 ms | 2.623757 ms | 8.881x |
2.138x |
| 128K / H32 | 2.384966 ms | 16.687767 ms | 5.395038 ms | 6.997x |
2.262x |

## Verification

Host checks:

- `python -m pytest tests/perf/test_gated_deltanet_prefill_roofline.py
-q`: `2 passed`
- `python -m pytest tests/test_validate_manifest.py -q`: `223 passed, 3
warnings`
- `pre-commit run --files tileops/perf/formulas.py
tests/perf/test_gated_deltanet_prefill_roofline.py`: passed
- `PYTHONPYCACHEPREFIX=<tmp> python -m py_compile
tileops/perf/formulas.py
tests/perf/test_gated_deltanet_prefill_roofline.py`: passed
- Manifest roofline smoke for `GatedDeltaNetPrefillFwdOp`: all listed
BTHD workloads now report nonzero FLOPs and bytes.

TileOpsGov CI docker image
`ghcr.io/tile-ai/tileops-runner:65dbc98-torch2.10` (`tilelang
0.1.11+cu129.git65dbc983`, `torch 2.10.0+cu129`):

- `python -m pytest tests/perf/test_gated_deltanet_prefill_roofline.py
-q`: `2 passed`
- Manifest roofline smoke for `GatedDeltaNetPrefillFwdOp`: all listed
BTHD workloads report nonzero FLOPs and bytes.
- `python -m pytest tests/test_validate_manifest.py -q`: `223 passed, 15
warnings`
- GPU smoke on H200:
`tests/ops/test_gated_deltanet_prefill.py::test_gated_deltanet_prefill_bthd_layout_matches_bhtd`
and `::test_gated_deltanet_prefill_bthd_blocksolve_path_matches_bhtd`:
`2 passed, 14 warnings`
… API (tile-ai#1661)

## Add payload support and implicit thread blocks to trace API

### Summary

This PR adds two enhancements to the trace API:

1. **Payload support for duration ranges** - Brings `trace.range()` in
line with `trace.record()` for payload capability. The `AnnoToken` now
carries payload through both `RANGE_BEGIN` and `RANGE_END` markers.

2. **Implicit thread blocks support** - Enables tracing in kernels using
simple `T.Kernel(..., threads=N)` syntax without explicit threadIdx.x
binding. Uses a new `__tl_thread_idx_x()` CUDA helper when explicit
binding is not present.

### Changes

#### 1. Payload Support

**API Changes**:
- **`trace.range(name, lane, payload=None)`** - Added optional `payload`
parameter
- **`trace.range_start(name, lane, payload=None)`** - Added optional
`payload` parameter
- Payload is recorded in both `RANGE_BEGIN` and `RANGE_END` events
- `AnnoToken` now stores payload value for the entire range scope

**Implementation**:
- Updated `AnnoToken` to store payload value
- Modified `open_range()` and `close_range()` to accept and emit payload
- Updated `RangeScope` to pass payload through context manager
- Simplified payload handling by letting `_emit()` handle None
conversion

**Usage Example**:
```python
# Tag iterations with their index
for i in range(N):
    with trace.range("iteration", payload=i):
        work(i)

# Payload appears in timeline UI (displayed separately and in hover tooltip)
```

#### 2. Implicit Thread Blocks Support

**Problem**: Kernels using simple `T.Kernel(1, threads=16)` syntax have
no explicit threadIdx.x binding, causing trace lowering to fail with
`KeyError: 'threadIdx.x'`.

**Solution**:
- Added `__tl_thread_idx_x()` CUDA device helper in
`tileops/trace/device.py`
- Modified `tileops/trace/passes.py` to detect missing threadIdx.x
binding
- Falls back to `T.call_extern("int32", "__tl_thread_idx_x")` when
binding not found
- Emits a warning to stderr when fallback is used

**Before** (required explicit binding):
```python
with T.Kernel(1, threads=(1, 16)):
    ty = T.thread_binding(0, 1, thread="threadIdx.y")
    tx = T.thread_binding(0, 16, thread="threadIdx.x")  # Required!
    with trace.range("work"):
        work(tx)
```

**After** (simple syntax works):
```python
with T.Kernel(1, threads=16):
    tx = T.get_thread_binding()  # Works now!
    with trace.range("work"):
        work(tx)
```

### Backward Compatibility

✅ Fully backward compatible:
- `payload` parameter is optional and defaults to `None` (recorded as 0)
- Explicit threadIdx.x binding still works as before
- Existing code continues to work without changes

```python
# Both still work
with trace.range("compute"):  # No payload
    work()

with T.Kernel(1, threads=(1, 16)):  # Explicit binding
    tx = T.thread_binding(0, 16, thread="threadIdx.x")
    with trace.range("work"):
        work(tx)
```

### Testing

**Test coverage** (`tests/trace/test_payload.py`):
- ✅ API signature verification (compilation tests)
- ✅ Constant payload with decode (payload=42)
- ✅ Dynamic payload with runtime PrimExpr (loop index → [0, 1, 2, 3])
- ✅ range_start/range_end with payload decode
- ✅ Backward compatibility without payload
- ✅ Implicit thread blocks with trace enabled/disabled
- ✅ Process-level trace state preservation (no test pollution)
- ✅ All tests marked with `pytest.mark.full` tier

**Test strategy**:
- GPU end-to-end verification with trace.decode()
- Tests verify payload is written to slots and can be decoded
- Tests verify implicit thread blocks fallback compiles and runs
- Precise assertions (e.g., "exactly one slice with payload=42")
- Coverage of both constant and runtime expression payloads

### Known Limitations

⚠️ **Unverified behavior**: Payload behavior inside `T.Pipelined()`
loops has not been validated. Compiler optimizations (such as
loop-invariant code motion or loop unrolling) may affect whether payload
values from individual iterations are captured.

**Recommendation**: Use payload for:
- ✅ Non-pipelined loops
- ✅ Conditional tracing
- ✅ High-level event tagging
- ✅ Simple kernels with implicit thread blocks

**Use with caution**:
- ⚠️ `T.Pipelined()` loop iterations (behavior not yet verified)

For precise pipeline profiling, consider using NVIDIA NSight Compute.


### Future Work

Potential improvements (out of scope for this PR):
- Add tests that decode trace slots and assert payload values
- Validate payload behavior in pipelined loops
- Compiler pass to preserve trace markers through optimizations
- Pipeline-aware trace API
- Eliminate warning for implicit thread blocks (make it silent)

### Checklist

- [x] API changes are backward compatible
- [x] Unit tests added (compilation and API verification)
- [x] Tests preserve trace state (no pollution)
- [x] Implicit thread blocks support working
- [x] Code follows project style
- [x] Known limitations documented
- [ ] Tests that decode and assert payload values (follow-up)
- [ ] Documentation updated (follow-up)

---

**Note**: This PR combines two related trace API enhancements. Both
features improve the usability of the trace system for common kernel
patterns. Feedback welcome on:
1. API design and payload propagation through token
2. Implicit thread blocks fallback strategy
3. Test coverage approach
@superAngGao superAngGao force-pushed the issue-1597-gdn-prefill-bench branch from ffcf855 to 95a312b Compare July 8, 2026 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bench Auto-created by labeler

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants