# Debug Fa Runtime Failure

> Use when NPU FlashAttention hangs (`ERT_CMD_STATE_TIMEOUT`) or produces NaN at head_dim ≥ 128. Discriminates the three known root causes (compile-flag mismatch, seq-first dk_chunks bug, true L1 overflow) via a symptom-classification table and applies the documented fix.

- Skill: `xilinx/debug-fa-runtime-failure` (Agent Skill)
- Install (CLI): `npx skillmds@latest add xilinx/debug-fa-runtime-failure`
- Raw SKILL.md: https://api.skillmd.com/api/skills/xilinx/debug-fa-runtime-failure/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: xilinx (https://skillmd.com/u/xilinx)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/xilinx/debug-fa-runtime-failure

---


## Purpose

NPU FlashAttention failures at head_dim ≥ 128 manifest in three
distinct ways with three distinct root causes. This skill walks the
diagnosis efficiently instead of bisecting from scratch — head_dim=128
deployments hit each of these.

Use this as a **diagnostic decision tree**, not a mechanical
fix-applier — match symptom to hypothesis BEFORE applying the
documented remedy.

## Knowledge base references

- `programming_examples/flash_attention/kernel_fusion_based/Makefile`
  — canonical `-D` flag conventions (the ground truth for compile
  flags)
- `programming_examples/flash_attention/kernel_fusion_based/attn_npu2.py`
  vs `.../attn_npu2_seqfirst.py` — the two Python builders (head-first
  vs seq-first); compile from the same C++ kernel but different IR
- `programming_examples/llms/llama_kernel_builder/external_kernels.py`
  — the FA compile API that derives per-tile flags correctly (the
  `compile_attn_npu2*` helpers)

## Triggers

ALL of these route here:

- `RuntimeError: Command failed to complete successfully (ERT_CMD_STATE_TIMEOUT)`
  from `cache.load_and_run("flash_attn", ...)` or `XRTRunner.run_test`
- All-NaN output from FA when inputs are well-formed (no NaN/Inf in)
- Compile passes but runtime output is constant garbage (e.g., all
  `49.0`, `844.0`, growing magnitudes — typical of softmax-not-running)

## Workflow

### Step 1: Classify the symptom

Run a minimal repro at the failing shape. Match the symptom:

| Symptom | Most-likely root cause | Jump to |
|---|---|---|
| HANG (timeout) at all `dk_chunks > 1` configs but PASS at `dk_chunks = 1` | Seq-first `dk_chunks > 1` upstream bug | Step 3 |
| NaN at any config (including ones the lit test passes with `uniform(0,4)` inputs) | `compile_attn_npu2*` flag mismatch (per-launch sizes baked into .o) | Step 2 |
| Garbage non-NaN output (large constant values, no softmax behavior) | Same as NaN — flag mismatch, just numerically different surface | Step 2 |
| HANG at one specific shape but PASS at smaller variants | True L1 overflow at the larger shape | Step 4 |

If the symptom doesn't fit any row, this is a new failure mode —
escalate per "Failure mode" at the bottom.

### Step 2: Verify .o flag conventions (most common — fixes NaN/garbage)

The `attn_npu2.cc` kernel's `lqp/lkp/dk/dv` defines are **per-tile**,
NOT per-launch. The Makefile's convention is canonical (verify against
this):

```makefile
LQP_TILE := $(shell echo $$(($(LQP) / $(NUM_Q_TILES))))
... -Dlqp=$(LQP_TILE) -Dlkp=$(LKP) \
    -Ddk=$(LKP) -Ddk_full=$(DK) \
    -Ddv=$(LKP) -Ddv_full=$(DV) ...
```

**Diagnostic**: diff your compile call against the Makefile. The FA
compile helper in
`programming_examples/llms/llama_kernel_builder/external_kernels.py` derives
`lqp_tile = lqp // num_q_tiles` internally and emits the right per-tile
flags.

**Remedy**: if your `.o` was compiled with per-launch flags, delete the
stale `.o` and the cached `flash_attn.elf`, then rebuild via the
external-kernels FA compile helper (which emits per-tile flags). Re-run.

### Step 3: Workaround the seq-first `dk_chunks > 1` upstream bug (Option C)

**Diagnostic**: `attn_npu2_seqfirst.py` (the seq-first Python builder)
has an untested `dk_chunks > 1` shim-DMA path that hangs at runtime.
Verify via bisect: every `dk_chunks=2` config hangs in seq-first,
regardless of (n_heads/n_kv, lq=lk). The HEAD-first kernel
`attn_npu2.py` at the same shape PASSES (`make run DK=128 DV=128
NUM_HEADS=32 NUM_KV_HEADS=8` → corr ≈ 0.996).

**Remedy** — Option C (head-first FA + host transposes):

1. Route the attention call through the head-first kernel
   (`attn_npu2.build_module(...)`) instead of seq-first, and add host
   transposes around it: reshape seq-first `[seq, n_heads*head_dim]` ↔
   head-first `[n_heads, seq, head_dim]` on the way in and out.
2. Wrap this as a helper that intercepts the `flash_attn` cached call so
   the rest of the pipeline stays seq-first and only the FA call sees
   head-first layout. Build it once in the deployment's `setup()` /
   block-compile path.

Cost: a few ms/layer host transpose. Gain: NPU FA actually runs (a
head_dim=128 deployment that fell back to CPU attention recovers a
multi-× warm prefill speedup once NPU FA works).

### Step 4: True L1 overflow (rare)

If both Step 2 and Step 3 are clean (correct flags, head-first
variant) and the kernel STILL hangs at one specific shape but PASSES
at smaller variants, you're hitting the actual 64 KB per-core L1
limit. Per-tile budget for FA at `(tile_size_q, lkp, dk_full, dv_full)`:

```
Q tile          : tile_size_q * lkp_per_dk_chunk * 2B
K tile (per dk) : lkp * lkp * 2B            (= 8 KB at lkp=64)
V tile (per dv) : lkp * lkp * 2B            (= 8 KB at lkp=64)
Gp accumulator  : tile_size_q * dv_full * 2B
misc (up,sp,r)  : ~2 KB
```

With `lkp=64`, the per-`dk_chunk` budget is small (~8 KB each) and
shared buffers are off (`lkp != dk_full` at hd=128). Sum stays well
under 64 KB at typical `tile_size_q ≤ 64`.

**Remedy**: drop `lqp` in the Python builder (which reduces
`tile_size_q = lqp / num_q_tiles`) and recompile.

True L1 overflow is rare for the shapes LLM deployments use. If you
hit it, also document the failing shape — it's a useful data point.

## Reusable bisect harness

When the symptom doesn't immediately fit Step 1's table, bisect across
(n_heads, n_kv_heads, lq=lk, dk) one axis at a time toward your
production config. The first axis that flips PASS → HANG/NaN tells
you which dimension is the offender.

The pattern is straightforward — invoke the external-kernels FA compile
helper to build the .o at a given shape, build the module via
`attn_npu2[_seqfirst].build_module(...)`, generate random inputs +
NumPy causal-SDPA reference, run via XRTRunner. Catch `TIMEOUT` →
"HANG"; cosine < threshold → "FAIL_NUMERICAL". A `<model>_phaseN_test.py`
script that exercises FA at the production shape is the worked example to
mirror.

## Verification

This skill is "successful" when the failing FA invocation produces
correct output (cosine ≥ Phase 2's head_dim-scaled threshold) and
runs without hang. Capture the resolution path in
`<model>/docs/development_progress/debug_log.md`.

## Failure mode (when this skill itself can't resolve)

If the symptom matches one of Step 1's rows but the documented remedy
in Step 2/3/4 doesn't resolve, OR the symptom doesn't fit any row:
this is a new failure mode not covered by current knowledge.

Escalate to the user with:
- The bisect matrix (which axis was varied, which configs PASS/HANG/NaN)
- The exact compile flags used
- The kernel cache state (was `.o` rebuilt? was `flash_attn.elf` re-cached?)

Do NOT silently wrap-fix or further-bisect for hours — the 3
documented root causes are well-characterized; a real new failure
mode deserves human triage.

## Update protocol

On successful diagnosis, append to
`<model>/docs/development_progress/debug_log.md`:

```
## debug-fa-runtime-failure recovery (YYYY-MM-DD)
- Failing shape: lq=X, lk=Y, dk=Z, hd=W, n_heads=A, n_kv=B
- Symptom: HANG / NaN / garbage
- Step matched: 2 / 3 / 4
- Fix applied: <one-line description>
- Verified: cosine ≥ <threshold> at production shape
```

