Skip to content
Back to skills

Passnet Triton Opt

ASecurity

Make a single PassNet Triton kernel fast AND numerically faithful: performance model (when a replacement can win at all), block/grid/warp tuning, autotune policy, launch overhead, and per-op numeric recipes to pass the dtype baseline tolerances. Use when a pass matches and is correct but speedup below expected, or when correctness fails by small numeric margins.

  • 247 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 9, 2026
developmentpythonrustgonodeperformance

Security analysis

A100/100

Scanned September 9, 2026

npx -y skills add VectorSpaceLab/AREX-Skill --skill passnet-triton-opt --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Passnet Triton Opt?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Passnet Triton Opt
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vectorspacelab-passnet-triton-opt/badge)](https://www.skillsdirectory.com/skills/vectorspacelab-passnet-triton-opt)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

SKILL.md
---
name: passnet-triton-opt
description: >
  Make a single PassNet Triton kernel fast AND numerically faithful: performance model
  (when a replacement can win at all), block/grid/warp tuning, autotune policy, launch
  overhead, and per-op numeric recipes to pass the dtype baseline tolerances. Use when a
  pass matches and is correct but speedup below expected, or when correctness fails by small
  numeric margins.
---

## 1. Performance model — know if a win is even possible

Measured costs (A100, torch 2.7.1 / triton 3.3.1; same order of magnitude on the eval box):

| component | cost per call |
|---|---|
| dynamo guards + FX graph interpretation + dispatch wrapper | ~25–45 µs on a 3-op graph; grows with node count (~150–230 µs seen on a 13-node graph) — calibrate from your first eval |
| Triton kernel Python launch (`kernel[grid](...)`) | ~19 µs each |
| `torch.empty_like` / `torch.empty` | ~2–3 µs each |
| each aten op you REMOVED from the graph | +5–25 µs back (small ops) or its real kernel time |

So per variant: `compiled_e2e ≈ eager_e2e − Σ(absorbed aten µs) + tax + Σ(your kernels µs)`.
Rules of thumb:
- Whole eager forward < ~150 µs and you can absorb ≤2 cheap ops → ceiling < 1. Ship a
  safe correct pass, accept ~0.8–0.95, spend your time on another bottleneck.
- Small or narrow fusions can match and be numerically correct yet still lose e2e when
  launch/framework overhead exceeds the eager work removed.
- Layout-only, view/reshape-only, split/fanout, tiny activation, and small reduction-like
  kernels can also lose even when their local kernel is correct: eager may treat the work
  as metadata, use a vendor-tuned primitive, or pay less framework overhead than the
  compiled path. Treat these as floor/downside caps unless they bridge substantial compute
  or remove an actual materialization.
- The lever is **ops absorbed per launch** (and avoiding extra launches/allocs), not
  shaving 10% off an already-tiny kernel.
- Big tensors (≥ a few MB; kernel ≥ 100 µs): overhead noise vanishes; now memory traffic
  is everything — a clean fused kernel that reads each input once and writes once usually
  beats the multi-pass eager chain by the ratio of memory passes saved.
- **The biggest wins can be ALGORITHMIC, not tuning** — but the default for a heavy conv/matmul
  is still "leave it in the library, fuse its tail," because the library is near-optimal inside
  the regime it tuned for. The clearest case where a rewrite beats it: a **non-overlapping
  convolution** (`stride == kernel_size`, `kernel_size > 1`, `padding == 0`) is a dense patch
  matmul while cuDNN runs a heavier general im2col path — a `tl.dot` patch-gather kernel
  (kernel-templates §13) can win big. It usually does NOT pay to rewrite a **1×1 conv** (already
  a cuBLAS-optimal pointwise GEMM), an **overlapping conv** (im2col blows up memory), a
  **depthwise/grouped conv**, or a **general dense matmul** — those are vendor sweet spots.
  Treat this as a prior, not a ban: the completed eval decides. Pursue any rewrite only after a
  floor is banked, and don't dismiss the non-overlapping case as "cuDNN, unbeatable" without
  checking `stride == kernel_size`.
- For large cat/stack/gather-style outputs, one giant kernel is not automatically best if it
  needs heavy branching or irregular index logic. A small fixed number of simple kernels can
  be faster and more timing-stable when each launch writes a large contiguous region. Compare
  sequential micro-bench signals, then trust the completed evaluation.
- Repeated round evidence favors long memory-bound elementwise and broadcast-affine chains
  when one legal single-output kernel absorbs multiple affine, activation, residual, layout,
  or normalization consumers. These are the first places to spend tuning effort after a
  correct floor because saved memory passes can amortize wrapper and launch overhead.
- Repeated completed evaluations also favor normalization-affine compute prefixes and
  concat-to-normalization-affine prefixes when they remove large memory passes in one launch.
  Treat kernel layout choices for concat branches as workload-dependent: a broad prefix can
  win strongly, but small/branchy cases or attempts to absorb extra producers can regress in
  score, correctness, or timing stability. Keep the best completed floor/prefix state and
  revert completed regressions instead of stacking more small kernels or sweeping blindly.
- Reduction-only regions and vendor-tail replacements still need completed-evaluation proof.
  A correct reduction kernel, a small tail behind a vendor op, or a region that replaces a
  vendor-tuned primitive can remain below eager even when local kernel timing looks plausible.
  Use the completed eval to decide keep/revert; do not infer success from the triage family.
- Compare `[Speedup][gpu]` vs `[Speedup][e2e]` in the eval log: gpu≈e2e&both<1 → on-stream
  gaps/launches dominate (absorb more ops / fewer launches); gpu>1 but e2e<1 → host-side
  overhead (reduce launches, drop autotune key churn, simplify wrapper Python).
- Only a completed evaluation can prove the final overhead-bound conclusion. Local
  micro-bench and `check_pattern --bench` are pre-flight signals; if the completed eval is
  matched and correct but below eager for the maximal legal region, stop or revert instead
  of sweeping launch-bound tuning knobs.

## 2. Kernel quality checklist (in order of impact)

1. **One read per input element, one write per output element.** No multi-launch chains
   when one kernel can hold the chain in registers. Exception: for heterogeneous
   direct-output cat/stack branches, compare a branch-heavy single kernel with grouped
   launches; choose by measured stable e2e speed, not by launch count alone.
2. **Contiguous innermost access**: thread `offs` should walk the last (stride-1) dim.
   For strided/permuted inputs pass strides; never `.contiguous()` (poison-illegal) —
   absorbing the transpose INTO the index math is free.
3. **Block sizing**: elementwise → BLOCK 1024 (n ≥ 1M: 2048, num_warps=8). Row kernels →
   `BLOCK_N = next_power_of_2(N)`, and start with **num_warps=4** — more warps on small
   rows HURTS (measured: 512-elem rows, warps=8 was 1.8× slower than warps=4). If
   N > 16384 switch to a looped accumulation (template 6 in kernel-templates).
   For tiny rows (≤1024 elems), SWEEP rows-per-program ∈ {1, 2, 4, 8} (2D tile) — the
   winner is shape-dependent (measured 1.45× for ROWS=4 at D=512/131k rows, but ROWS=1
   best on a 768-wide LayerNorm); never assume, always micro-bench (§5).
4. **Grid**: prefer 1D `cdiv(n, BLOCK)`; for row kernels one program per row is fine up
   to ~256k rows. Avoid grids of 1–4 programs on big inputs (underutilization) — split.
5. **num_stages**: only matters for `tl.dot`/looped kernels (3–4); elementwise ignore.
6. **Avoid recompilation churn**: every distinct `tl.constexpr` value (incl. BLOCK chosen
   from shape) compiles a new binary during warmup. 2–3 specializations fine; per-shape
   formulas over 100-variant samples → timeout risk. Derive constexprs from a small fixed
   set (e.g. BLOCK_N = next_pow2 clamped to {128, 512, 1024, 4096}).
7. **Autotune policy**: `@triton.autotune` is legal and runs during the untimed warmup
   (poison-safe). Use ≤4–6 configs, `key=['N']`-style coarse keys. SKIP autotune when the
   sample has many graph variants (tuning cost × variants × 600 s timeout) or when the
   kernel is launch-bound anyway. A hand-picked config from one local micro-benchmark is
   usually within 5%.
8. **Scalar args beat tensor args**: precompute `n`, `C`, `HW`, strides in the wrapper
   (metadata access is poison-legal) — never load shape info from tensors in-kernel.
9. **Reduce wrapper Python**: no dict lookups in hot path beyond the route `if`; compute
   grid inline; allocate with `torch.empty` (not zeros — don't pay a memset you'll
   overwrite).

## 3. Numeric fidelity — passing t=-5 baseline tolerances

Baselines: fp32 rtol 1.3e-6/atol 1e-5 (tight — mirror eager's algorithm), fp16 rtol 1e-3,
bf16 rtol 1.6e-2 (loose). The same kernel must satisfy ALL dtype variants.

Recipes (these match aten's CUDA implementations):
- **Always compute in fp32** (`.to(tl.float32)` after load), store with
  `.to(out_ptr.dtype.element_ty)`. For fp16/bf16 inputs this reproduces aten (which also
  accumulates in fp32) to ≤1 ulp.
- Reductions: `tl.sum`/`tl.max` over a row in one block ≈ aten's order closely enough for
  fp32 rtol 1.3e-6 at typical sizes (N ≤ 16k). For looped accumulation keep ONE fp32
  accumulator (don't reorder into multiple partial schemes unnecessarily).
- softmax: subtract row max (aten does), exp, divide by sum. Don't use exp2 shortcuts on
  fp32 variants.
- layer_norm/batch_norm: biased variance (÷N), `1/sqrt(var+eps)` in fp32, then scale/shift.
- mean: `sum/N` (single division at the end, not running mean).
- GELU exact: `0.5x(1+erf(x/√2))` via `tl.math.erf`; tanh GELU only when model.py says
  `approximate='tanh'`.
- sigmoid/silu: `1/(1+exp(-x))` fp32 — fine at all baselines.
- `x.pow(2)` → `x*x`; `torch.rsqrt` → `1.0/tl.sqrt` (matches aten rsqrt within 1 ulp; if
  a strict fp32 variant complains, try `tl.math.rsqrt`).
- Integer/bool tensors: compare with `torch.equal` (atol/rtol=0 conceptually) — your
  kernel must be EXACT (indices, masks, argmax-style ops: replicate tie-breaking by
  first-occurrence).
- Output dtype must equal eager's per output ([Datatype] check), including intermediate
  `.to(torch.float32)` nodes you absorbed — store in the dtype eager would have at the
  region's output.

Diagnosing accuracy failures from eval logs: `[Correctness][max_diff]` magnitude vs the
table above (parse_eval_log.py does this) —
- max_diff ~1e-3·|values| on fp16 only → fp16 rounding mismatch: ensure fp32 internal math.
- fails ONLY fp32 variants → algorithmic mismatch (reduction order, formula) — mirror
  aten's exact sequence.
- max_diff huge (≥1) → indexing/masking bug, not numerics: check strides, `other=` values
  contaminating reductions (use `-inf` for max, `0` for sum, and mask BEFORE divide),
  uninitialized output regions (mask coverage), `offs` overflow on >2^31 elements
  (use `tl.int64` offsets when numel > 2e9).
- NaN/Inf only in compiled → division by masked-out zero lanes; `tl.where` the mask before
  the division, not after.

## 4. Launch-overhead reduction (when e2e-bound)

- Merge kernels (one launch per region; the whole point of fusion).
- Do not add a tiny independent kernel after a correct-but-slow state unless it absorbs
  enough real eager work to pay for another launch. If a completed eval shows the added
  region lowers score, revert to the best completed state and stop that line.
- Avoid per-call `next_power_of_2` recompute churn → precompute in wrapper; constexpr set
  small (see 2.6).
- Don't return tuples/lists from the wrapper unless the pattern output needs it.
- Buffer reuse across calls (cache `out` keyed by shape in a module dict) saves ~2 µs but
  risks aliasing bugs if the model returns your output AND mutates downstream — only as a
  last resort, verify with full eval.
- CUDA graphs / `torch.compile` of the wrapper / `no_dispatch` tricks: NOT allowed
  (torch.* calls blocked / hacking). Don't.

## 5. Micro-benchmark before full eval (GPU, seconds)

```python
import torch, time
def bench(fn, *args, iters=500):
    for _ in range(20): fn(*args)
    torch.cuda.synchronize(); t0 = time.perf_counter()
    for _ in range(iters): fn(*args)
    torch.cuda.synchronize(); return (time.perf_counter() - t0) / iters * 1e6  # µs
```
Compare: (a) eager region ops chained, (b) your wrapper. Build inputs with the shapes /
dtypes from weight_meta (use the checker's replay). Target: wrapper ≤ eager_region − 40 µs
for small graphs; for ms-scale kernels target the memory-traffic bound (bytes moved /
~1.5 TB/s on A100 ≈ achievable µs).

`check_pattern.py --bench` (passnet-feedback) automates exactly this comparison per
variant; trust the full eval for the final number (it adds guards/FX costs that local
micro-bench misses).

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.