Use this skill for Ascend operator performance work on all files under any triton_ascend directory (**/triton_ascend/**).
Multi-round iteration discipline (frozen tests, task contract, when to stop): fla-optimization-loop. MR packaging: fla-mr-readiness.
Collection must use this skill's generic scripts — do not copy torch_npu.profiler boilerplate per op.
Environment: Use the Python/NPU environment already active in the current terminal (including any activated conda/venv). Run collection, analysis, and benchmarks in the same shell; do not spawn a new shell or switch environments mid-workflow. If the terminal has no NPU stack loaded yet, activate the project's Ascend environment first, then continue in that same session. Metrics, failure modes, code index: reference.md. Past kernel notes: cases.md.
Make the target backend semantically correct before optimizing; never hide missing capability or kernel bugs behind a Torch fallback. What generalizes: UB modeling, grid splits, layout/precision, and verification. Values like BC=16, K slabs of 64, or specific mem_mult are starting points only — do not copy them as rules.
Hard constraint (NPU launch params): Ascend Triton kernels do not supportnum_warps or num_stages. During optimization these kwargs must never appear in @triton.jit launches, triton.autotune configs — do not copy them from CUDA Triton. Tune via tiles, grid, layout, fusion/split, and UB budget only.
Progress checklist
- [ ] 1. Freeze semantics, workload, and baseline latency
- [ ] 2. Collect with generic scripts (first pass: PipeUtilization)
- [ ] 3. Parse CSVs and classify the bottleneck
- [ ] 4. Triton-Ascend optimize for that bottleneck (MemoryUB if needed)
- [ ] 5. Correctness gate + synchronized benchmark
- [ ] 6. Re-profile to confirm metrics, then decide whether to continue
1. Freeze semantics and baseline
Locate the public entry, @dispatch, default impl, and closest Ascend impl; list layout, dtype, fixed/varlen, head mapping, fwd/bwd, and optional args.
Keep Torch reference implementations only in tests/benchmarks as the oracle.
Pick shape/dtype/fwd±bwd; freeze tests, tolerances, and shapes during optimization — do not change tests to manufacture speedups.
Baseline with synchronized timing (warmup + torch.npu.synchronize() + repeats); confirm the target NPU kernel runs, not a Torch fallback.
Do not change the public API to fit the kernel; register backends under IS_NPU with lazy imports; verifiers must state real support ranges.
2. Generic collection (required)
Scripts live under .agents/skills/fla-ascend-performance/scripts/ (run from that directory or set PYTHONPATH).
workload_only.py only defines workload() — no profiler boilerplate:
def workload():
y = op(...)
y.backward(grad)
Library usage (when not using --exec-file):
from profile_npu import profile_callable
def workload():
y = op(...)
y.backward(grad)
trace_dir = profile_callable(
workload,
name="my_op",
out_dir="npu_prof",
aic_metrics="PipeUtilization", # or MemoryUB / L2Cache / ...
)
Default schedule: wait=0, warmup=1, active=1, repeat=1. One aic_metrics per run; start with PipeUtilization, collect MemoryUB separately for UB bandwidth.
3. Diagnose bottlenecks
cd .agents/skills/fla-ascend-performance
python scripts/analyze_profile.py path/to/*_profiling_* --kernel-filter <substr>
op_statistic: who owns Total Time; is the target kernel the real hotspot?
Low aiv_vec_ratio (~0.75) while MemoryUB is not saturated; larger tiles UB-overflow
Dual DMA paths live in UB
Runtime block_ptr vs masked load: host-split with tl.constexpr so each launch DCE's the other (cases.md § causal_conv1d)
Colloquial “CUDA utilization” → read Cube/MAC (aic_mac_ratio). Host UB model complements the profiler — see reference.md.
Prioritize fixes by Duration share in kernel_details / op_statistic (largest hotspot first). Low pipe ratios on a dominant kernel usually mean room remains on that pipe.
4. Optimize (Triton-Ascend)
Change only levers that match the bottleneck; one hypothesis per round. Before tuning, classify the issue: compile failure / UB overflow / grid limit / numeric error / real performance bottleneck — do not treat all five the same way.
UB and tiles
UB is usually the primary constraint, not theoretical FLOPs. Enumerate peak live tiles (fp32 accum, transpose copies, masks, temp dots).
peak ≈ memory_multiplier * tiled_elements * dtype_size; comment where the multiplier comes from.
Use fla.utils.ascend_ub_manager (compute_row_tile_block_size, etc.); do not hard-code capacity; keep ~0.75–0.85 safety margin.
Prefer power-of-two tiles; matrix ops prefer 16-alignment; model fwd/bwd separately (bwd usually smaller tiles).
If a fused kernel cannot fit a reliable UB budget, split stages + scratch/recompute — do not keep an inevitably overflowing live set for “fusion”.
Persistently unused safe budget → consider non-PoT tiles / calibrate mem_mult; near 100% and still slow → look at pipe/bandwidth.
Layout, grid, numerics
Innermost block-pointer dim should be contiguous; tl.make_block_ptr + boundary_check; @input_guard for layout — do not emulate arbitrary strides in-kernel.
Gate g along T (critical on Ascend): if g is [B, T, HV], host g.transpose(1, 2).contiguous() and load via G_T_CONTIG + stride-1 g_ptr (see g-contiguous-loading.md). Stride-HV gathers in bwd hot loops can be 10×–35× slower than contiguous loads; HV==1 needs no transpose. Match fwd pointer math; keep T_seq before varlen overwrites T.
Distinct shapes (e.g. HV==1, layout flags) get separate paths — no expensive hot-loop branches.
Grid product cap ASCEND_MAX_GRID_DIM=65535: host-split with iter_axis_launch_chunks, pass *_OFFSET; after varlen slicing, zero the matching offset — never slice and also add a global offset. UB and grid are independent constraints.
1D core-grid (prefer when there are many independent tiles and multi-axis grids need host chunking): flatten work into task_num and schedule with for task_id in tl.range(core_id, task_num, num_core) (or range(pid, total_tasks, num_programs)). Decode task_id → tile indices inside the kernel. One launch, no ASCEND_MAX_GRID_DIM host loop, better load balance when task_num is irregular. Keep do_not_specialize on T / task_num / num_core / dynamic extents.
Match core count to the bound pipe: Cube-bound → grid=(num_aicore,) via get_device_properties()["num_aicore"]. Vector-bound (conv, layernorm, rotary) → get_multiprocessor_count (num_vectorcore on NPU; A2 is 48 vector vs 24 Cube). Launching a Vector kernel on num_aicore leaves half the vector cores idle.
In a core-grid task loop, rebind local pointers each iteration (q_ptr = q + …); do not accumulate with in-place ptr += across tasks — Ascend Triton can mis-compile that pattern.
int64 before multiply on runtime indices: program IDs and grid-derived values (i_t, i_b, NT = cdiv(T, BT)) are runtime int32 or narrower. do_not_specialize on T makes NT runtime, but i_t * stride also wraps when T is specialized (packed conv: i_t * BT then offset * D). (NT - 1) * DH_CS wraps past 2³¹ before a trailing .to(tl.int64). Example: DH_CS=HV*K*V, K=V=128, HV=64, BT=64 → overflow at NT>2048 (T>131K). Packed offset * D: T>2³¹/D (D=4096 → T>524K). Cast the index first with tl.cast (not .to on specialized ints): tl.cast(i_t, tl.int64) * BT, tl.cast(i_b, tl.int64) * T, tl.cast(B, tl.int64) * T, tl.cast(NT - 1, tl.int64) * DH_CS. Kernel args B/T are constexpr — B.to(tl.int64) is AttributeError("'constexpr' object has no attribute 'to'"); i_t/i_b can fold to constexpr when NT=1. tl.load(...).to(tl.int64) on cu_seqlens is fine. Never (i_b * T).to(tl.int64) or ((NT - 1) * DH_CS).to(tl.int64).
make_block_ptr offsets stay int32: Triton rejects int64 offsets/block_shape. Flattened pointer math (bos * D, t0 * D, i_b * stride) uses int64; pass i_t * BT (int32) as the block row offset. Do not feed t0 into make_block_ptr. Case: causal_conv1d.
Varlen cu_seqlens → int64 for pointer math: host dtype is often torch.long, but tests also pass int32; load as tl.int64 either way. Loading .to(tl.int32) then (bos * HV + i_hv) * V overflows well before bos hits 2³¹ (HV=32, V=4096 → safe bos ≈ 16K). Pattern: bos, eos = tl.load(cu_seqlens + i_n).to(tl.int64), tl.load(cu_seqlens + i_n + 1).to(tl.int64); T_cur = (eos - bos).to(tl.int32). Non-varlen: bos = tl.cast(i_b, tl.int64) * T (CUDA/repo often writes (i_b * T).to(tl.int64), which still wraps if i_b * T exceeds 2³¹). Alternative when T_cur only needs int32: load bos as int32 but cast the index before the large stride — tl.cast(bos, tl.int64) * HV + i_h then * K. (bos * HV + i_h).to(tl.int64) * K only fixes * K/* V (HV is small); bos * HV itself can still wrap.
Reductions / recurrence / grads use fp32 accum, cast on store; sensitive solves: input_precision='ieee' / allow_tf32=False; mask before exp on gated paths; keep a consistent exp/exp2 base.
Ascend tl.dot clobbers the left operand: on NPU, tl.dot(lhs, rhs, …) may overwrite lhs in UB (CUDA Triton does not). Any later read of that tile (second lhs, rhs, store) sees corrupted data unless you reload from GM or copy with tile + 0.0before the first lhs dot. Full per-kernel catalog: cases.md § tl.dot lhs clobber. Symptom: silent numeric drift vs Torch oracle with no compile error.
Audit checklist for new/changed kernels: (1) rg 'tl\.dot\(' fla/ops/**/triton_ascend/** — only 8 op files use tl.dot; (2) for each lhs tile, flag lhs→lhs, lhs→rhs/store, or post-dot copy; (3) prefer GM reload for one reuse between stages, + 0.0 for tight multi-dot sequences; (4) re-run tests/ops/test_gdn_kernels.py + op-specific kernel tests.
Upstream: lhs clobber is a Triton-Ascend backend limitation (UB capacity / in-place matmul), not intentional API. Durable fix belongs in the compiler (preserve lhs or emit a diagnostic on post-dot read). Track via the Triton-Ascend / Ascend backend issue tracker.
For separable gate differences, compute exp2(gs)[:, None] / exp2(gc)[None, :] instead of exp2(gs[:, None] - gc[None, :]) to replace a matrix of exponentials with two vectors. Verify numerics on the target compiler; multiplying by exp2(-gc) can produce materially different Ascend results.
Constexpr-split mutually exclusive DMA paths (critical on Ascend): a runtime if is_tail_chunk that chooses make_block_ptr vs masked tl.load keeps both paths live in UB. Peak UB ≈ sum of both; Vector cannot saturate even when MemoryUB bandwidth is free; larger tiles then fail compile. Host-split the last tile into a second launch with tl.constexpr TAIL_MODE (0 = never tail / block_ptr only, 1 = always masked, 2 = runtime for varlen / NT==1) so each compile DCE's the unused path. Case: causal_conv1d.
MTE DMA past packed allocation: make_block_ptr whose block end overshoots packed B*T rows faults MTE (DDR address out of range). Use masked load/store on the last chunk, or the constexpr split above so bulk never overshoots. Halo windows (BT+W-1) overshoot even sooner — count the halo in the tail predicate.
Do not OR a constexpr optional-pointer flag with a runtime check: if USE_INITIAL_STATE or i_t*BT < W still lowers the else and compiles initial_state + … when the pointer is None. Nest: if not FLAG: … elif runtime: … else: ….
tl.extract_slice / tl.insert_slice: sliding-window taps without extra GM loads (causal conv). Some triton-ascend versions expose them only via triton.language.extra.cann.extension — shim onto tl if missing. Preloading every tap tile overflows UB; load inside the static_range or one BT+W-1 window + slice.
Weight [D, W] → host transpose(0,1).contiguous() to [W, D] for stride-1 channel block_ptr (same idea as G_T_CONTIG). Odd D that cannot be tiled with a power-of-two BD that divides D and BD>=16 falls back to the legacy multi-axis path.
Fusion, compile, varlen
Fuse only stages that share loads, cut traffic, and keep live set under control; split independent grad chains to ease UB.
Producer → consumer on the same output (e.g. inter o += q@h then intra o += A@v with ACCUMULATE_OUTPUT): if both need the same q (and live set fits), fuse into one kernel — keep b_o / b_A in UB, single store. Profiler cue: two kernels own the op and MTE is high from the intermediate o writeback. If fused peak UB overflows, keep the split; do not force fusion.
When fused live set is dominated by fixed tiles (e.g. BT×BT + BT×BV), fix the Cube-aligned outer tile (BV) and autotune the K-slab (BK) rather than host-hardcoding both.
Multi-tile contribs to one grad: fp32 partials + deterministic finalize; atomics sparingly. tl.debug_barrier only for same-program deps.
do_not_specialize=['T'] (and other dynamic launch extents); kill runtime branches with tl.constexpr / triton.heuristics.
No num_warps / num_stages anywhere: NPU does not support them. Omit from kernel call sites, autotune config dicts, and wrappers. Do not leave them commented-out “for CUDA parity”; delete them.
Varlen is first-class: reuse prepare_chunk_indices / prepare_chunk_offsets. With 1D core-grid, flatten over total_chunks and map global_t → (i_n, i_t) via chunk_offsets (largest i_n with chunk_offsets[i_n] <= global_t). Tests cover empty tails, non-aligned lengths, multi-length, and fixed/varlen equivalence.
Failure modes and repo paths: reference.md. Detailed past cases: cases.md.
5. Verification loop
Each round, in order:
Single kernel vs Torch oracle (fp16/bf16, fwd+bwd).
Shape matrix: small/large T, non-aligned tiles, head sharing, gate/state, fixed/varlen.
Any unsupported triton-ascend ops encountered and workarounds used
Whether another round is warranted (per fla-optimization-loop stop criteria)
Generalizable fixes discovered during optimization belong in this skill (SKILL.md, references/reference.md, or references/cases.md) in a separate doc commit — not bundled into a perf PR.
Review checklist
Same algorithm/control flow as CUDA reference (tiling/grid/layout adaptations only)
No Torch fallback on production paths; unsupported cases error / verifier rejects
No num_warps / num_stages in Ascend kernel launches, autotune configs, or wrappers
Backend registration, lazy import, public signatures correct
Peak live tiles estimated; tiles from shared helpers + safety margin
Grid ≤ 65535 or 1D core-grid (num_aicore Cube / num_vectorcore Vector); host-split offsets not double-counted with varlen; task-loop pointers rebound each iteration
Runtime block_ptr vs masked DMA: constexpr-split so bulk DCE's the unused path; tail DMA does not overshoot packed B*T (include halo)
Optional-pointer constexpr flags are nested, not or-ed with runtime checks (None ptr must not compile)
Block pointers contiguous innermost; gate g uses G_T_CONTIG when [B,T,HV] (see g-contiguous-loading.md); tail boundary_check
fp32 accum consistent with output/exp base; fusion worth the complexity (no gratuitous ACCUMULATE_OUTPUT writeback when UB allows)
Reused tl.dot left-hand tiles: GM reload or tile + 0.0before first lhs dot (post-dot copy invalid); see cases.md § tl.dot catalog
fwd/bwd/varlen/layout branches covered; no unwritten regions under NaN poisoning
Runtime indices (NT, i_t, i_b, B, program IDs) via tl.cast(..., tl.int64)before stride / BT / D multiply — including packed offset * D. Not gated on do_not_specialize. Do not call .to(tl.int64) on specialized kernel args (constexpr has no .to)
Varlen bos/eos from cu_seqlens loaded as tl.int64; T_cur = (eos - bos).to(tl.int32) only; non-varlen tl.cast(i_b, tl.int64) * T
make_block_ptr offsets/block_shape stay int32 (i_t * BT); int64 is only for flattened ptr + offset * stride
Optional-arg paths exercised (e.g. use_g True/False with g=None reference) when PR touches gated and ungated paths
Did not weaken tests/tolerances/benchmarks for “wins”; synced bench + re-profile on target NPU
Round summary includes pipe/UB metrics (template above)
Anti-patterns
Copying profiler boilerplate into every test_*.py
Treating async launch time as latency; missing warmup/synchronize
Expecting Pipe and MemoryUB columns from a single run
Tuning MTE before confirming the fused NPU kernel is hit
Loosening tolerances, dropping cases, or editing benchmarks to fake speedups
Adding or keeping num_warps / num_stages on Ascend paths (unsupported; not a tuning lever)
Hiding unsupported triton-ascend ops without documenting workarounds
Leaving a runtime is_tail_chunk (or similar) between block_ptr and masked DMA — both stay in UB
Launching a Vector-bound kernel on num_aicore (half the vector cores idle on A2)
if CONSTEXPR_FLAG or runtime: around an optional pointer — else still compiles when the ptr is None
B.to(tl.int64) / i_t.to(tl.int64) on specialized or folded constexpr ints (constexpr has no .to); use tl.cast
Passing int64 t0 as make_block_ptr offsets (offsets/block_shape must be int32)
Ad-hoc workload output dir: npu_prof/ (new collection must use the generic scripts)
1---2name: fla-ascend-performance3description: Guidelines for Ascend NPU kernel / Triton-Ascend backend performance work in the FLA repo. Covers profiling with torch_npu, PipeUtilization/MemoryUB CSV analysis, Cube/Vector/MTE/UB bottleneck diagnosis, and kernel optimization (UB tiling, grid splits, fusion/split, varlen, G_T_CONTIG gate loading, constexpr DMA-path split / TAIL_MODE, extract_slice, MTE OOB, int32 address overflow, tl.cast vs constexpr .to, make_block_ptr int32 offsets, correctness gates, tl.dot left-operand clobber). NPU kernels must not use num_warps/num_stages. Per-kernel catalog: references/cases.md (incl. causal_conv1d core-grid). Use when working on NPU profiling, kernel_details/op_statistic, aic_metrics, fla triton_ascend backends (ops or modules), g transpose stride-1, UB overflow, dual-path DCE, grid limits, int64 pointer math, tl.dot reuse, or Ascend performance.4---56# FLA Ascend NPU: Profiling → Bottlenecks → Optimization78Use this skill for Ascend operator performance work on all files under any `triton_ascend` directory (`**/triton_ascend/**`).910Multi-round iteration discipline (frozen tests, task contract, when to stop): **`fla-optimization-loop`**. MR packaging: **`fla-mr-readiness`**.1112Collection **must** use this skill's **generic scripts** — do not copy `torch_npu.profiler` boilerplate per op.1314Environment: **Use the Python/NPU environment already active in the current terminal** (including any activated conda/venv). Run collection, analysis, and benchmarks in the same shell; do not spawn a new shell or switch environments mid-workflow. If the terminal has no NPU stack loaded yet, activate the project's Ascend environment first, then continue in that same session. Metrics, failure modes, code index: [reference.md](references/reference.md). Past kernel notes: [cases.md](references/cases.md).1516Make the target backend semantically correct before optimizing; never hide missing capability or kernel bugs behind a Torch fallback. What generalizes: UB modeling, grid splits, layout/precision, and verification. Values like `BC=16`, K slabs of 64, or specific `mem_mult` are starting points only — do not copy them as rules.1718**Hard constraint (NPU launch params):** Ascend Triton kernels **do not support** `num_warps` or `num_stages`. During optimization these kwargs must **never** appear in `@triton.jit` launches, `triton.autotune` configs — do not copy them from CUDA Triton. Tune via tiles, grid, layout, fusion/split, and UB budget only.1920## Progress checklist2122```23- [ ] 1. Freeze semantics, workload, and baseline latency24- [ ] 2. Collect with generic scripts (first pass: PipeUtilization)25- [ ] 3. Parse CSVs and classify the bottleneck26- [ ] 4. Triton-Ascend optimize for that bottleneck (MemoryUB if needed)27- [ ] 5. Correctness gate + synchronized benchmark28- [ ] 6. Re-profile to confirm metrics, then decide whether to continue29```3031## 1. Freeze semantics and baseline32331. Locate the public entry, `@dispatch`, default impl, and closest Ascend impl; list layout, dtype, fixed/varlen, head mapping, fwd/bwd, and optional args.342. Keep Torch reference implementations only in tests/benchmarks as the oracle.353. Pick shape/dtype/fwd±bwd; freeze tests, tolerances, and shapes during optimization — do not change tests to manufacture speedups.364. Baseline with synchronized timing (warmup + `torch.npu.synchronize()` + repeats); confirm the target NPU kernel runs, not a Torch fallback.375. Do not change the public API to fit the kernel; register backends under `IS_NPU` with lazy imports; verifiers must state real support ranges.3839## 2. Generic collection (required)4041Scripts live under `.agents/skills/fla-ascend-performance/scripts/` (run from that directory or set `PYTHONPATH`).4243| Script | Role |44|--------|------|45| `scripts/profile_npu.py` | Trace any `workload()` |46| `scripts/analyze_profile.py` | Parse `op_statistic` / `kernel_details` |4748```bash49SKILL_DIR=.agents/skills/fla-ascend-performance50cd "$SKILL_DIR"5152python scripts/profile_npu.py \53 --name my_op --out-dir npu_prof \54 --metrics PipeUtilization --analyze \55 --kernel-filter my_kernel_substr \56 --exec-file path/to/workload_only.py57```5859`workload_only.py` only defines `workload()` — no profiler boilerplate:6061```python62def workload():63 y = op(...)64 y.backward(grad)65```6667Library usage (when not using `--exec-file`):6869```python70from profile_npu import profile_callable7172def workload():73 y = op(...)74 y.backward(grad)7576trace_dir = profile_callable(77 workload,78 name="my_op",79 out_dir="npu_prof",80 aic_metrics="PipeUtilization", # or MemoryUB / L2Cache / ...81)82```8384Default schedule: `wait=0, warmup=1, active=1, repeat=1`. One `aic_metrics` per run; start with `PipeUtilization`, collect `MemoryUB` separately for UB bandwidth.8586## 3. Diagnose bottlenecks8788```bash89cd .agents/skills/fla-ascend-performance90python scripts/analyze_profile.py path/to/*_profiling_* --kernel-filter <substr>91```92931. **`op_statistic`**: who owns Total Time; is the target kernel the real hotspot?942. **`kernel_details`** (by Duration): read pipe / UB columns.9596| Signal | Bottleneck | Prefer |97|--------|------------|--------|98| High `aiv_vec_ratio`, Cube≈0 | Vector-bound | Larger row tile, less scalar, fuse load/store |99| High `aic_mac_ratio` / `cube_utilization` | Cube-bound | Better matmul tiles/alignment, less non-Cube prelude |100| High `mte2/mte3_ratio`, low compute | Memory-move-bound | More reuse, fewer writebacks; check strides — **gate `g` stride-HV gather** often 10×+ slower ([g-contiguous-loading.md](references/g-contiguous-loading.md)) |101| High `scalar_ratio` | Scalar-bound | Vectorize, kill branches, heuristics |102| High UB bw under MemoryUB, low vec/mac | UB bandwidth saturated | Larger tiles / more fusion |103| Low target Ratio, many tiny ops | Unfused / fallback | Fix dispatch and fusion first |104| Two kernels share `o` + high MTE | Intermediate writeback | Fuse producer/consumer if UB fits; else keep split |105| Frequent host grid chunking | Launch / grid-product overhead | Prefer 1D core-grid (`num_aicore` Cube / `num_vectorcore` Vector) + flat `task_id` |106| Low `aiv_vec_ratio` (~0.75) while MemoryUB is *not* saturated; larger tiles UB-overflow | Dual DMA paths live in UB | Runtime `block_ptr` vs masked load: host-split with `tl.constexpr` so each launch DCE's the other ([cases.md § causal_conv1d](references/cases.md#causal_conv1dpy--1d-core-grid--constexpr-dma-split)) |107108Colloquial “CUDA utilization” → read **Cube/MAC** (`aic_mac_ratio`). Host UB model complements the profiler — see [reference.md](references/reference.md).109110Prioritize fixes by **Duration share** in `kernel_details` / `op_statistic` (largest hotspot first). Low pipe ratios on a dominant kernel usually mean room remains on that pipe.111112## 4. Optimize (Triton-Ascend)113114Change only levers that match the bottleneck; one hypothesis per round. Before tuning, classify the issue: **compile failure / UB overflow / grid limit / numeric error / real performance bottleneck** — do not treat all five the same way.115116### UB and tiles117118- UB is usually the primary constraint, not theoretical FLOPs. Enumerate peak live tiles (fp32 accum, transpose copies, masks, temp dots).119- `peak ≈ memory_multiplier * tiled_elements * dtype_size`; comment where the multiplier comes from.120- Use `fla.utils.ascend_ub_manager` (`compute_row_tile_block_size`, etc.); do not hard-code capacity; keep ~0.75–0.85 safety margin.121- Prefer power-of-two tiles; matrix ops prefer 16-alignment; model fwd/bwd separately (bwd usually smaller tiles).122- If a fused kernel cannot fit a reliable UB budget, split stages + scratch/recompute — do not keep an inevitably overflowing live set for “fusion”.123- Persistently unused safe budget → consider non-PoT tiles / calibrate `mem_mult`; near 100% and still slow → look at pipe/bandwidth.124125### Layout, grid, numerics126127- Innermost block-pointer dim should be contiguous; `tl.make_block_ptr` + `boundary_check`; `@input_guard` for layout — do not emulate arbitrary strides in-kernel.128- **Gate `g` along T (critical on Ascend)**: if `g` is `[B, T, HV]`, host `g.transpose(1, 2).contiguous()` and load via `G_T_CONTIG` + stride-1 `g_ptr` (see [g-contiguous-loading.md](references/g-contiguous-loading.md)). Stride-`HV` gathers in bwd hot loops can be **10×–35× slower** than contiguous loads; HV==1 needs no transpose. Match fwd pointer math; keep `T_seq` before varlen overwrites `T`.129- Distinct shapes (e.g. `HV==1`, layout flags) get separate paths — no expensive hot-loop branches.130- Grid product cap `ASCEND_MAX_GRID_DIM=65535`: host-split with `iter_axis_launch_chunks`, pass `*_OFFSET`; after varlen slicing, zero the matching offset — never slice and also add a global offset. UB and grid are independent constraints.131- **1D core-grid** (prefer when there are many independent tiles and multi-axis grids need host chunking): flatten work into `task_num` and schedule with `for task_id in tl.range(core_id, task_num, num_core)` (or `range(pid, total_tasks, num_programs)`). Decode `task_id` → tile indices inside the kernel. One launch, no `ASCEND_MAX_GRID_DIM` host loop, better load balance when `task_num` is irregular. Keep `do_not_specialize` on `T` / `task_num` / `num_core` / dynamic extents.132- **Match core count to the bound pipe**: Cube-bound → `grid=(num_aicore,)` via `get_device_properties()["num_aicore"]`. Vector-bound (conv, layernorm, rotary) → `get_multiprocessor_count` (`num_vectorcore` on NPU; A2 is 48 vector vs 24 Cube). Launching a Vector kernel on `num_aicore` leaves half the vector cores idle.133- In a core-grid task loop, **rebind local pointers each iteration** (`q_ptr = q + …`); do not accumulate with in-place `ptr +=` across tasks — Ascend Triton can mis-compile that pattern.134- **int64 before multiply on runtime indices**: program IDs and grid-derived values (`i_t`, `i_b`, `NT = cdiv(T, BT)`) are runtime int32 or narrower. `do_not_specialize` on `T` makes `NT` runtime, but `i_t * stride` also wraps when `T` is specialized (packed conv: `i_t * BT` then `offset * D`). `(NT - 1) * DH_CS` wraps past 2³¹ before a trailing `.to(tl.int64)`. Example: DH_CS=`HV*K*V`, K=V=128, HV=64, BT=64 → overflow at NT>2048 (T>131K). Packed `offset * D`: T>2³¹/D (D=4096 → T>524K). Cast the index first with `tl.cast` (not `.to` on specialized ints): `tl.cast(i_t, tl.int64) * BT`, `tl.cast(i_b, tl.int64) * T`, `tl.cast(B, tl.int64) * T`, `tl.cast(NT - 1, tl.int64) * DH_CS`. Kernel args `B`/`T` are constexpr — `B.to(tl.int64)` is `AttributeError("'constexpr' object has no attribute 'to'")`; `i_t`/`i_b` can fold to constexpr when NT=1. `tl.load(...).to(tl.int64)` on `cu_seqlens` is fine. Never `(i_b * T).to(tl.int64)` or `((NT - 1) * DH_CS).to(tl.int64)`.135- **`make_block_ptr` offsets stay int32**: Triton rejects int64 `offsets/block_shape`. Flattened pointer math (`bos * D`, `t0 * D`, `i_b * stride`) uses int64; pass `i_t * BT` (int32) as the block row offset. Do not feed `t0` into `make_block_ptr`. Case: [causal_conv1d](references/cases.md#causal_conv1dpy--1d-core-grid--constexpr-dma-split).136- **Varlen `cu_seqlens` → int64 for pointer math**: host dtype is often `torch.long`, but tests also pass `int32`; load as `tl.int64` either way. Loading `.to(tl.int32)` then `(bos * HV + i_hv) * V` overflows well before `bos` hits 2³¹ (HV=32, V=4096 → safe `bos` ≈ 16K). Pattern: `bos, eos = tl.load(cu_seqlens + i_n).to(tl.int64), tl.load(cu_seqlens + i_n + 1).to(tl.int64); T_cur = (eos - bos).to(tl.int32)`. Non-varlen: `bos = tl.cast(i_b, tl.int64) * T` (CUDA/repo often writes `(i_b * T).to(tl.int64)`, which still wraps if `i_b * T` exceeds 2³¹). Alternative when `T_cur` only needs int32: load `bos` as int32 but cast **the index** before the large stride — `tl.cast(bos, tl.int64) * HV + i_h` then `* K`. `(bos * HV + i_h).to(tl.int64) * K` only fixes `* K`/`* V` (HV is small); `bos * HV` itself can still wrap.137- Reductions / recurrence / grads use fp32 accum, cast on store; sensitive solves: `input_precision='ieee'` / `allow_tf32=False`; mask before exp on gated paths; keep a consistent `exp`/`exp2` base.138- **Ascend `tl.dot` clobbers the left operand**: on NPU, `tl.dot(lhs, rhs, …)` may overwrite `lhs` in UB (CUDA Triton does not). Any later read of that tile (second lhs, rhs, store) sees corrupted data unless you reload from GM or copy with `tile + 0.0` **before** the first lhs dot. Full per-kernel catalog: [cases.md § tl.dot lhs clobber](references/cases.md#tldot-lhs-clobber--repo-wide-case-catalog). Symptom: silent numeric drift vs Torch oracle with no compile error.139- **Audit checklist for new/changed kernels**: (1) `rg 'tl\.dot\(' fla/ops/**/triton_ascend/**` — only 8 op files use `tl.dot`; (2) for each lhs tile, flag lhs→lhs, lhs→rhs/store, or post-dot copy; (3) prefer GM reload for one reuse between stages, `+ 0.0` for tight multi-dot sequences; (4) re-run `tests/ops/test_gdn_kernels.py` + op-specific kernel tests.140- **Upstream**: lhs clobber is a Triton-Ascend backend limitation (UB capacity / in-place matmul), not intentional API. Durable fix belongs in the compiler (preserve lhs or emit a diagnostic on post-dot read). Track via the Triton-Ascend / Ascend backend issue tracker.141- For separable gate differences, compute `exp2(gs)[:, None] / exp2(gc)[None, :]` instead of `exp2(gs[:, None] - gc[None, :])` to replace a matrix of exponentials with two vectors. Verify numerics on the target compiler; multiplying by `exp2(-gc)` can produce materially different Ascend results.142- **Constexpr-split mutually exclusive DMA paths** (critical on Ascend): a runtime `if is_tail_chunk` that chooses `make_block_ptr` vs masked `tl.load` keeps **both** paths live in UB. Peak UB ≈ sum of both; Vector cannot saturate even when MemoryUB bandwidth is free; larger tiles then fail compile. Host-split the last tile into a second launch with `tl.constexpr TAIL_MODE` (`0` = never tail / block_ptr only, `1` = always masked, `2` = runtime for varlen / `NT==1`) so each compile DCE's the unused path. Case: [causal_conv1d](references/cases.md#causal_conv1dpy--1d-core-grid--constexpr-dma-split).143- **MTE DMA past packed allocation**: `make_block_ptr` whose block end overshoots packed `B*T` rows faults MTE (`DDR address out of range`). Use masked load/store on the last chunk, or the constexpr split above so bulk never overshoots. Halo windows (`BT+W-1`) overshoot even sooner — count the halo in the tail predicate.144- **Do not OR a constexpr optional-pointer flag with a runtime check**: `if USE_INITIAL_STATE or i_t*BT < W` still lowers the else and compiles `initial_state + …` when the pointer is `None`. Nest: `if not FLAG: … elif runtime: … else: …`.145- **`tl.extract_slice` / `tl.insert_slice`**: sliding-window taps without extra GM loads (causal conv). Some triton-ascend versions expose them only via `triton.language.extra.cann.extension` — shim onto `tl` if missing. Preloading every tap tile overflows UB; load inside the `static_range` or one `BT+W-1` window + slice.146- **Weight `[D, W]` → host `transpose(0,1).contiguous()` to `[W, D]`** for stride-1 channel `block_ptr` (same idea as G_T_CONTIG). Odd `D` that cannot be tiled with a power-of-two `BD` that divides `D` and `BD>=16` falls back to the legacy multi-axis path.147148### Fusion, compile, varlen149150- Fuse only stages that share loads, cut traffic, and keep live set under control; split independent grad chains to ease UB.151- **Producer → consumer on the same output** (e.g. inter `o += q@h` then intra `o += A@v` with `ACCUMULATE_OUTPUT`): if both need the same `q` (and live set fits), fuse into one kernel — keep `b_o` / `b_A` in UB, single store. Profiler cue: two kernels own the op and MTE is high from the intermediate `o` writeback. If fused peak UB overflows, keep the split; do not force fusion.152- When fused live set is dominated by fixed tiles (e.g. `BT×BT` + `BT×BV`), fix the Cube-aligned outer tile (`BV`) and **autotune** the K-slab (`BK`) rather than host-hardcoding both.153- Multi-tile contribs to one grad: fp32 partials + deterministic finalize; atomics sparingly. `tl.debug_barrier` only for same-program deps.154- `do_not_specialize=['T']` (and other dynamic launch extents); kill runtime branches with `tl.constexpr` / `triton.heuristics`.155- **No `num_warps` / `num_stages` anywhere**: NPU does not support them. Omit from kernel call sites, autotune config dicts, and wrappers. Do not leave them commented-out “for CUDA parity”; delete them.156- Varlen is first-class: reuse `prepare_chunk_indices` / `prepare_chunk_offsets`. With 1D core-grid, flatten over `total_chunks` and map `global_t → (i_n, i_t)` via `chunk_offsets` (largest `i_n` with `chunk_offsets[i_n] <= global_t`). Tests cover empty tails, non-aligned lengths, multi-length, and fixed/varlen equivalence.157158Failure modes and repo paths: [reference.md](references/reference.md). Detailed past cases: [cases.md](references/cases.md).159160## 5. Verification loop161162Each round, in order:1631641. Single kernel vs Torch oracle (fp16/bf16, fwd+bwd).1652. Shape matrix: small/large T, non-aligned tiles, head sharing, gate/state, fixed/varlen.1663. End-to-end tests; confirm dispatch hits `triton_ascend`.1674. Frozen full pytest gate (incl. NaN poisoning); on failure, stop — do not claim speedups.1685. Synchronized benchmark (latency/throughput, fwd and fwd+bwd); re-profile with the same `aic_metrics` and confirm Duration/pipe/UB move as expected.1696. Metrics unchanged → reclassify bottleneck or switch metrics; do not pile unrelated changes.170171Prefer: `tests/ops/test_gdn_kernels.py`, `tests/ops/test_solve_tril.py`, `tests/modules/test_conv.py` (causal_conv1d), `tests/utils/test_ascend_ub_manager.py`, `python -m benchmarks.ops.verify --op <op> --base <ref>` (`--gate-k` is a quick signal only).172173### Round summary template174175After re-profile, report:176177- Target kernel Duration (before → after)178- Pipe ratios: Cube/MAC, Vector, scalar, MTE1, MTE2, MTE3179- UB bandwidth (if MemoryUB run collected)180- Any unsupported triton-ascend ops encountered and workarounds used181- Whether another round is warranted (per `fla-optimization-loop` stop criteria)182183Generalizable fixes discovered during optimization belong in this skill (`SKILL.md`, `references/reference.md`, or `references/cases.md`) in a separate doc commit — not bundled into a perf PR.184185## Review checklist186187- [ ] Same algorithm/control flow as CUDA reference (tiling/grid/layout adaptations only)188- [ ] No Torch fallback on production paths; unsupported cases error / verifier rejects189- [ ] No `num_warps` / `num_stages` in Ascend kernel launches, autotune configs, or wrappers190- [ ] Backend registration, lazy import, public signatures correct191- [ ] Peak live tiles estimated; tiles from shared helpers + safety margin192- [ ] Grid ≤ 65535 **or** 1D core-grid (`num_aicore` Cube / `num_vectorcore` Vector); host-split offsets not double-counted with varlen; task-loop pointers rebound each iteration193- [ ] Runtime `block_ptr` vs masked DMA: constexpr-split so bulk DCE's the unused path; tail DMA does not overshoot packed `B*T` (include halo)194- [ ] Optional-pointer constexpr flags are nested, not `or`-ed with runtime checks (None ptr must not compile)195- [ ] Block pointers contiguous innermost; **gate `g` uses G_T_CONTIG** when `[B,T,HV]` (see [g-contiguous-loading.md](references/g-contiguous-loading.md)); tail `boundary_check`196- [ ] fp32 accum consistent with output/exp base; fusion worth the complexity (no gratuitous `ACCUMULATE_OUTPUT` writeback when UB allows)197- [ ] Reused `tl.dot` left-hand tiles: GM reload or `tile + 0.0` **before** first lhs dot (post-dot copy invalid); see [cases.md § tl.dot catalog](references/cases.md#tldot-lhs-clobber--repo-wide-case-catalog)198- [ ] fwd/bwd/varlen/layout branches covered; no unwritten regions under NaN poisoning199- [ ] Runtime indices (`NT`, `i_t`, `i_b`, `B`, program IDs) via `tl.cast(..., tl.int64)` **before** stride / `BT` / `D` multiply — including packed `offset * D`. Not gated on `do_not_specialize`. Do not call `.to(tl.int64)` on specialized kernel args (`constexpr` has no `.to`)200- [ ] Varlen `bos`/`eos` from `cu_seqlens` loaded as `tl.int64`; `T_cur = (eos - bos).to(tl.int32)` only; non-varlen `tl.cast(i_b, tl.int64) * T`201- [ ] `make_block_ptr` offsets/block_shape stay int32 (`i_t * BT`); int64 is only for flattened `ptr + offset * stride`202- [ ] Optional-arg paths exercised (e.g. `use_g` True/False with `g=None` reference) when PR touches gated and ungated paths203- [ ] Did not weaken tests/tolerances/benchmarks for “wins”; synced bench + re-profile on target NPU204- [ ] Round summary includes pipe/UB metrics (template above)205206## Anti-patterns207208- Copying profiler boilerplate into every `test_*.py`209- Treating async launch time as latency; missing warmup/synchronize210- Expecting Pipe and MemoryUB columns from a single run211- Tuning MTE before confirming the fused NPU kernel is hit212- Loosening tolerances, dropping cases, or editing benchmarks to fake speedups213- Adding or keeping `num_warps` / `num_stages` on Ascend paths (unsupported; not a tuning lever)214- Hiding unsupported triton-ascend ops without documenting workarounds215- Leaving a runtime `is_tail_chunk` (or similar) between `block_ptr` and masked DMA — both stay in UB216- Launching a Vector-bound kernel on `num_aicore` (half the vector cores idle on A2)217- `if CONSTEXPR_FLAG or runtime:` around an optional pointer — else still compiles when the ptr is None218- `B.to(tl.int64)` / `i_t.to(tl.int64)` on specialized or folded constexpr ints (`constexpr` has no `.to`); use `tl.cast`219- Passing int64 `t0` as `make_block_ptr` offsets (`offsets/block_shape` must be int32)220221## Related files222223- Collect / analyze: `scripts/profile_npu.py`, `scripts/analyze_profile.py`224- Metrics, failure modes, code index: [references/reference.md](references/reference.md)225- Past kernel case notes: [references/cases.md](references/cases.md)226- **Gate `g` stride-1 loading (G_T_CONTIG)**: [g-contiguous-loading.md](references/g-contiguous-loading.md)227- **causal_conv1d 1D core-grid + constexpr DMA split**: [cases.md § causal_conv1d](references/cases.md#causal_conv1dpy--1d-core-grid--constexpr-dma-split)228- Ascend-specific traps (DMA dual-path UB, None-ptr compile, `constexpr` `.to`, int64 `block_ptr` offsets): [TRAPS.md](references/TRAPS.md)229- Ad-hoc workload output dir: `npu_prof/` (new collection must use the generic scripts)
Run npx skillmds@latest add fla-org/fla-ascend-performance in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Guidelines for Ascend NPU kernel / Triton-Ascend backend performance work in the FLA repo. Covers profiling with torch_npu, PipeUtilization/MemoryUB CSV analysis, Cube/Vector/MTE/UB bottleneck diagnosis, and kernel optimization (UB tiling, grid splits, fusion/split, varlen, G_T_CONTIG gate loading, constexpr DMA-path split / TAIL_MODE, extract_slice, MTE OOB, int32 address overflow, tl.cast vs constexpr .to, make_block_ptr int32 offsets, correctness gates, tl.dot left-operand clobber). NPU kernels must not use num_warps/num_stages. Per-kernel catalog: references/cases.md (incl. causal_conv1d core-grid). Use when working on NPU profiling, kernel_details/op_statistic, aic_metrics, fla triton_ascend backends (ops or modules), g transpose stride-1, UB overflow, dual-path DCE, grid limits, int64 pointer math, tl.dot reuse, or Ascend performance. It is listed under Data & Analytics on SkillMD.
This skill has not completed SkillMD's automated safety review yet. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
fla-org (@fla-org) published this skill. Their other Agent Skills are listed on their SkillMD profile.