FLA Ascend NPU: Profiling → Bottlenecks → Optimization
SkillFiles & storageGuides your agent through profiling and optimizing Ascend NPU kernels in the FLA repo.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the FLA Ascend NPU: Profiling → Bottlenecks → Optimization skill
About this capability
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,
What this skill tells your AI
The instructions your AI receives, as published by fla-org/flash-linear-attention in .agents/skills/fla-ascend-performance/SKILL.md and read by ahel’s review.
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 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.
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_NPUwith 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).
| Script | Role |
|---|---|
scripts/profile_npu.py | Trace any workload() |
scripts/analyze_profile.py | Parse op_statistic / kernel_details |
SKILL_DIR=.agents/skills/fla-ascend-performance
cd "$SKILL_DIR"
python scripts/profile_npu.py \
--name my_op --out-dir npu_prof \
--metrics PipeUtilization --analyze \
--kernel-filter my_kernel_substr \
--exec-file path/to/workload_only.py
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?kernel_details(by Duration): read pipe / UB columns.
| Signal | Bottleneck | Prefer |
|---|---|---|
High aiv_vec_ratio, Cube≈0 | Vector-bound | Larger row tile, less scalar, fuse load/store |
High aic_mac_ratio / cube_utilization | Cube-bound | Better matmul tiles/alignment, less non-Cube prelude |
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) |
High scalar_ratio | Scalar-bound | Vectorize, kill branches, heuristics |
| High UB bw under MemoryUB, low vec/mac | UB bandwidth saturated | Larger tiles / more fusion |
| Low target Ratio, many tiny ops | Unfused / fallback | Fix dispatch and fusion first |
Two kernels share o + high MTE | Intermediate writeback | Fuse producer/consumer if UB fits; else keep split |
| Frequent host grid chunking | Launch / grid-product overhead | Prefer 1D core-grid (num_aicore Cube / num_vectorcore Vector) + flat task_id |
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_guardfor layout — do not emulate arbitrary strides in-kernel. - Gate
galong T (critical on Ascend): ifgis[B, T, HV], hostg.transpose(1, 2).contiguous()and load viaG_T_CONTIG+ stride-1g_ptr(see g-contiguous-loading.md). Stride-HVgathers in bwd hot loops can be 10×–35× slower than contiguous loads; HV==1 needs no transpose. Match fwd pointer math; keepT_seqbefore varlen overwritesT. - 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 withiter_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_numand schedule withfor task_id in tl.range(core_id, task_num, num_core)(orrange(pid, total_tasks, num_programs)). Decodetask_id→ tile indices inside the kernel. One launch, noASCEND_MAX_GRID_DIMhost loop, better load balance whentask_numis irregular. Keepdo_not_specializeonT/task_num/num_core/ dynamic extents. - Match core count to the bound pipe: Cube-bound →
grid=(num_aicore,)viaget_device_properties()["num_aicore"]. Vector-bound (conv, layernorm, rotary) →get_multiprocessor_count(num_vectorcoreon NPU; A2 is 48 vector vs 24 Cube). Launching a Vector kernel onnum_aicoreleaves half the vector cores idle. - In a core-grid task loop, rebind local pointers each iteration (
q_ptr = q + …); do not accumulate with in-placeptr +=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_specializeonTmakesNTruntime, buti_t * stridealso wraps whenTis specialized (packed conv:i_t * BTthenoffset * D).(NT - 1) * DH_CSwraps 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). Packedoffset * D: T>2³¹/D (D=4096 → T>524K). Cast the index first withtl.cast(not.toon 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 argsB/Tare constexpr —B.to(tl.int64)isAttributeError("'constexpr' object has no attribute 'to'");i_t/i_bcan fold to constexpr when NT=1.tl.load(...).to(tl.int64)oncu_seqlensis fine. Never(i_b * T).to(tl.int64)or((NT - 1) * DH_CS).to(tl.int64). make_block_ptroffsets stay int32: Triton rejects int64offsets/block_shape. Flattened pointer math (bos * D,t0 * D,i_b * stride) uses int64; passi_t * BT(int32) as the block row offset. Do not feedt0intomake_block_ptr. Case: causal_conv1d.- Varlen
cu_seqlens→ int64 for pointer math: host dtype is oftentorch.long, but tests also passint32; load astl.int64either way. Loading.to(tl.int32)then(bos * HV + i_hv) * Voverflows well beforeboshits 2³¹ (HV=32, V=4096 → safebos≈ 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 ifi_b * Texceeds 2³¹). Alternative whenT_curonly needs int32: loadbosas int32 but cast the index before the large stride —tl.cast(bos, tl.int64) * HV + i_hthen* K.(bos * HV + i_h).to(tl.int64) * Konly fixes* K/* V(HV is small);bos * HVitself 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 consistentexp/exp2base. - Ascend
tl.dotclobbers the left operand: on NPU,tl.dot(lhs, rhs, …)may overwritelhsin 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 withtile + 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 usetl.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.0for tight multi-dot sequences; (4) re-runtests/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 ofexp2(gs[:, None] - gc[None, :])to replace a matrix of exponentials with two vectors. Verify numerics on the target compiler; multiplying byexp2(-gc)can produce materially different Ascend results. - Constexpr-split mutually exclusive DMA paths (critical on Ascend): a runtime
if is_tail_chunkthat choosesmake_block_ptrvs maskedtl.loadkeeps 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 withtl.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_ptrwhose block end overshoots packedB*Trows 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 < Wstill lowers the else and compilesinitial_state + …when the pointer isNone. 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 viatriton.language.extra.cann.extension— shim ontotlif missing. Preloading every tap tile overflows UB; load inside thestatic_rangeor oneBT+W-1window + slice.- Weight
[D, W]→ hosttranspose(0,1).contiguous()to[W, D]for stride-1 channelblock_ptr(same idea as G_T_CONTIG). OddDthat cannot be tiled with a power-of-twoBDthat dividesDandBD>=16falls 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@hthen intrao += A@vwithACCUMULATE_OUTPUT): if both need the sameq(and live set fits), fuse into one kernel — keepb_o/b_Ain UB, single store. Profiler cue: two kernels own the op and MTE is high from the intermediateowriteback. 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_barrieronly for same-program deps. do_not_specialize=['T'](and other dynamic launch extents); kill runtime branches withtl.constexpr/triton.heuristics.- No
num_warps/num_stagesanywhere: 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 overtotal_chunksand mapglobal_t → (i_n, i_t)viachunk_offsets(largesti_nwithchunk_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.
- End-to-end tests; confirm dispatch hits
triton_ascend. - Frozen full pytest gate (incl. NaN poisoning); on failure, stop — do not claim speedups.
- Synchronized benchmark (latency/throughput, fwd and fwd+bwd); re-profile with the same
aic_metricsand confirm Duration/pipe/UB move as expected. - Metrics unchanged → reclassify bottleneck or switch metrics; do not pile unrelated changes.
Prefer: 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).
Round summary template
After re-profile, report:
- Target kernel Duration (before → after)
- Pipe ratios: Cube/MAC, Vector, scalar, MTE1, MTE2, MTE3
- UB bandwidth (if MemoryUB run collected)
- Any unsupported triton-ascend ops encountered and workarounds used
- Whether another round is warranted (per
fla-optimization-loopstop 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_stagesin 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_aicoreCube /num_vectorcoreVector); host-split offsets not double-counted with varlen; task-loop pointers rebound each iteration - Runtime
block_ptrvs masked DMA: constexpr-split so bulk DCE's the unused path; tail DMA does not overshoot packedB*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
guses G_T_CONTIG when[B,T,HV](see g-contiguous-loading.md); tailboundary_check - fp32 accum consistent with output/exp base; fusion worth the complexity (no gratuitous
ACCUMULATE_OUTPUTwriteback when UB allows) - Reused
tl.dotleft-hand tiles: GM reload ortile + 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) viatl.cast(..., tl.int64)before stride /BT/Dmultiply — including packedoffset * D. Not gated ondo_not_specialize. Do not call.to(tl.int64)on specialized kernel args (constexprhas no.to) - Varlen
bos/eosfromcu_seqlensloaded astl.int64;T_cur = (eos - bos).to(tl.int32)only; non-varlentl.cast(i_b, tl.int64) * T -
make_block_ptroffsets/block_shape stay int32 (i_t * BT); int64 is only for flattenedptr + offset * stride - Optional-arg paths exercised (e.g.
use_gTrue/False withg=Nonereference) 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_stageson Ascend paths (unsupported; not a tuning lever) - Hiding unsupported triton-ascend ops without documenting workarounds
- Leaving a runtime
is_tail_chunk(or similar) betweenblock_ptrand 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 NoneB.to(tl.int64)/i_t.to(tl.int64)on specialized or folded constexpr ints (constexprhas no.to); usetl.cast- Passing int64
t0asmake_block_ptroffsets (offsets/block_shapemust be int32)
Related files
- Collect / analyze:
scripts/profile_npu.py,scripts/analyze_profile.py - Metrics, failure modes, code index: references/reference.md
- Past kernel case notes: references/cases.md
- Gate
gstride-1 loading (G_T_CONTIG): g-contiguous-loading.md - causal_conv1d 1D core-grid + constexpr DMA split: cases.md § causal_conv1d
- Ascend-specific traps (DMA dual-path UB, None-ptr compile,
constexpr.to, int64block_ptroffsets): TRAPS.md - Ad-hoc workload output dir:
npu_prof/(new collection must use the generic scripts)
Signals
- GitHub stars
- 6k
- Forks
- 702
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
fla-ascend-performance- Source
- github.com/fla-org/flash-linear-attention