计划执行与检查节点
executing-plans
按既定实施计划完成工作,保持过程检查和反馈。
passnet-triton-opt【效率与协作】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 nume…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
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:
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.[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).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.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.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).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.tl.dot/looped kernels (3–4); elementwise ignore.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}).@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%.n, C, HW, strides in the wrapper
(metadata access is poison-legal) — never load shape info from tensors in-kernel.if; compute
grid inline; allocate with torch.empty (not zeros — don't pay a memset you'll
overwrite).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):
.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.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).1/sqrt(var+eps) in fp32, then scale/shift.sum/N (single division at the end, not running mean).0.5x(1+erf(x/√2)) via tl.math.erf; tanh GELU only when model.py says
approximate='tanh'.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).torch.equal (atol/rtol=0 conceptually) — your
kernel must be EXACT (indices, masks, argmax-style ops: replicate tie-breaking by
first-occurrence)..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) —
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).tl.where the mask before
the division, not after.next_power_of_2 recompute churn → precompute in wrapper; constexpr set
small (see 2.6).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.torch.compile of the wrapper / no_dispatch tricks: NOT allowed
(torch.* calls blocked / hacking). Don't.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).