Skip to content

[AIR] Replace the memtile fan-in/fan-out daisy chain with a per-slot rendezvous - #2002

Draft
erwei-xilinx wants to merge 2 commits into
mainfrom
fix-memtile-fanin-rendezvous-lock
Draft

erwei-xilinx wants to merge 2 commits into
mainfrom
fix-memtile-fanin-rendezvous-lock

Conversation

@erwei-xilinx

Copy link
Copy Markdown
Collaborator

The second defect behind #1984 — the one that survives the DECODE_STACK fix in #1998. gemma4-e2b's PLE decode hangs in firmware TDR on NPU2 Krackan: 7 of 20 dispatches at default power mode, ERT_CMD_STATE_TIMEOUT and ERT_CMD_STATE_ABORT at the 7.0/9.0 s watchdog.

Cause

A cyclic wait between AIR's serialized fan-in chain lock and a shared switchbox arbiter.

buf143/buf162 on mem_tile_2_1 gather the four projection columns: a chain-lock fan-in set, 4 writers, 2 ping-pong slots. pickChainBdLocks daisy-chained them, cap → W0 → W1 → W2 → W3 → R → cap, imposing a compile-time total order on arrivals that are independent at runtime — four free-running herds in four columns.

All four gather streams egress to the shim row and travel west. Switchbox (2,0) has 9 masters and 6 arbiters, so three pairs must share, and the pathfinder put W0 and W2 on arbiter 1 (W1 and W3 on arbiter 2). A master port is backpressured until its S2MM channel's lock frees, and an arbiter granted to a stalled packet cannot grant another slave. So when ping-pong lets the east columns run a round ahead, W2's packet wins arbiter 1, stalls on W1's chain token, and holds the arbiter W0 needs — W0 being the writer the whole chain is waiting on.

It predicts the per-herd bisect cell by cell:

blk0 PP blk1 PP pass
no yes 0/20
yes yes 4/20
yes no 20/20
no no 19/20

Only run-ahead of the late stages matters, because an earlier stage holding the arbiter cannot close a cycle — the later stage was waiting on it anyway. The PLE is the amplifier, not the cause: it lengthens the reader's credit-return path so the stall window is wide enough to overtake.

Fix

Each buffer slot gets its own (capacity, signal) pair instead of chaining the stages. For slot s, the multi side acquires cap[s] by 1 and releases sig[s] by 1; the single side acquires sig[s] by N and releases cap[s] by N, with cap[s] primed to N. Each slot is a counted barrier and the N participants are mutually unordered, so an early arrival costs latency and can never hold a resource an earlier stage needs. Run-ahead stays bounded to pp_slots.

Indexing by slot is what makes a counted lock work here, and is why the daisy chain looked necessary: one shared counting semaphore cannot bound per-participant run-ahead, and an AIE2 BD has exactly one lock-acquire and one lock-release field, so an N-way join is not expressible either. Per-slot counted locks need neither.

Scope

  • Refeed buffers keep the daisy chain (ChainLockSet::serialized). Their credits come from air.refeed_count, not the participant count, and they never get a ping-pong twin. memtile_chain_lock_v2_refeed.mlir passes untouched.
  • air.no_chain_lock stays honored for fan-out only, so tagged designs keep their lowering. It is now a no-op in practice, but changing that isn't this PR's business.
  • use_lock_race_condition_fix_v2 defaults off; only fused_decode and fused_decode_ple enable it.

Validation

On device at default power mode, same tree and geometry apart from the lock template:

build result
daisy chain (control) 7/20 — 11 TIMEOUT, 2 ABORT
per-slot rendezvous 40/40, mean 65.1 ms

Both emit identical logits. No performance change — the control's non-hanging dispatches run 64.5–64.6 ms against the fix's 65.1 ms mean.

check-air-mlir 557 tests, 539 passed, 0 failed. check-air-cpp 1/1. clang-format 17 clean. fused_decode, the other v2 user, compiles clean.

Four tests pinned the old chain and now assert the new invariant — every participant takes the same lock for a given slot. memtile_chain_lock_v2_fanin_n3.mlir is new: it pins the credit count to the participant count rather than the constant 4, and covers an odd N.

Follow-up

run_npu2_sweep.lit has all eight contexts in --expect-fail and run_npu2_profile.lit skips decode, both marked temporary. Flipping them needs #1998, #2000 and this all landed, so it's a separate PR.

🤖 Generated with Claude Code

…rendezvous

gemma4-e2b's PLE decode hangs in firmware TDR on an NPU2 Krackan -- 7 of 20
dispatches at default power mode, ERT_CMD_STATE_TIMEOUT and
ERT_CMD_STATE_ABORT at the 7.0/9.0 s watchdog.  This is the second defect
behind issue #1984, the one that survives the DECODE_STACK fix.  It is a
cyclic wait between AIR's serialized fan-in chain lock and a shared switchbox
arbiter.

buf143/buf162 on mem_tile_2_1 gather the four projection columns: a chain-lock
fan-in set, 4 writers, 2 ping-pong slots.  pickChainBdLocks daisy-chained
them, cap -> W0 -> W1 -> W2 -> W3 -> R -> cap, which imposes a compile-time
total order on arrivals that are independent at runtime -- four free-running
herds in four columns.  Nothing makes the two orders agree.

All four gather streams egress to the shim row and travel west.  Switchbox
(2,0) has 9 masters and 6 arbiters, so three pairs must share, and the
pathfinder put W0 and W2 on arbiter 1 (W1 and W3 on arbiter 2).  A master port
is backpressured until its S2MM channel's lock frees, and an arbiter granted
to a stalled packet cannot grant another slave.  So when ping-pong lets the
east columns run a round ahead, W2's packet wins arbiter 1, stalls waiting for
W1's chain token, and holds the arbiter W0 needs -- W0 being the writer the
whole chain is waiting on.  Circular wait.

That predicts the per-herd bisect exactly.  Denying ping-pong, (blk0, blk1) =
(no, yes) 0/20, (yes, yes) 4/20, (yes, no) 20/20, (no, no) 19/20.  Only
run-ahead of the LATE stages matters, because an earlier stage holding the
arbiter cannot close a cycle: the later stage was waiting on it anyway.  The
PLE is the amplifier, not the cause -- it lengthens the reader's credit-return
path so the stall window is wide enough to overtake.

So give each buffer SLOT its own (capacity, signal) pair instead of chaining
the stages.  For slot s the multi side acquires cap[s] by 1 and releases
sig[s] by 1; the single side acquires sig[s] by N and releases cap[s] by N,
with cap[s] primed to N.  Each slot is a counted barrier and the N
participants are mutually unordered, so an early arrival costs latency and can
never hold a resource an earlier stage needs.  The cycle is structurally
impossible however the pathfinder assigns amsels.  Run-ahead stays bounded to
pp_slots: the (pp_slots+1)'th transfer wraps to slot 0, whose capacity the
single side has not replenished.

Indexing by slot is what makes a counted lock work here, and is why the
daisy chain looked necessary.  One shared counting semaphore cannot bound
per-participant run-ahead -- a single writer takes every credit and laps the
reader -- and an AIE2 BD has exactly one lock-acquire and one lock-release
field, so an N-way join is not expressible either.  Per-slot counted locks
need neither.

Refeed buffers keep the daisy chain, behind ChainLockSet::serialized.  Their
credits come from air.refeed_count rather than the participant count and they
never get a ping-pong twin, so the skew cannot arise; the refeed test passes
untouched.  air.no_chain_lock stays honored for fan-out only, so designs
carrying it keep their lowering -- with the participants no longer serialized
it is now a no-op in practice, but silently changing what a tagged buffer
lowers to is not this commit's business.

Measured on device at default power mode, same tree and geometry apart from
the lock template: daisy chain 7/20, per-slot rendezvous 40/40, both emitting
the same logits.  No performance change -- the control's non-hanging
dispatches run 64.5-64.6 ms against the fix's 65.1 ms mean.

Four tests pinned the old chain and now assert the new invariant, that every
participant takes the SAME lock for a given slot.
memtile_chain_lock_v2_fanin_n3.mlir is new: it pins the credit count to the
participant count rather than the constant 4, and covers an odd N.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 19, 2026 03:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The rendezvous protocol has an unresolved correctness concern, and several tests do not enforce distinct locks for each slot.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Replaces memtile fan-in/fan-out daisy-chain locks with per-slot counted rendezvous locks.

Changes:

  • Adds per-slot capacity/signal locks and ping-pong handling.
  • Updates DMA lock selection and counts.
  • Refreshes fan-in, fan-out, opt-out, and odd-participant tests.
File Reviewed change Review status
mlir/​test/​Conversion/​AIRToAIE/​memtile_no_chain_lock_fanin_ignored.mlir Updates fan-in opt-out expectations. Reviewed
mlir/​test/​Conversion/​AIRToAIE/​memtile_chain_lock_v2_pingpong.mlir Tests slot-specific ping-pong locks. Add explicit slot-lock distinctness assertion.
mlir/​test/​Conversion/​AIRToAIE/​memtile_chain_lock_v2_fanout.mlir Tests fan-out rendezvous behavior. Add explicit slot-lock distinctness assertion.
mlir/​test/​Conversion/​AIRToAIE/​memtile_chain_lock_v2_fanin.mlir Tests fan-in rendezvous behavior. Add explicit slot-lock distinctness assertion.
mlir/​test/​Conversion/​AIRToAIE/​memtile_chain_lock_v2_fanin_n3.mlir Covers odd participant counts. Reviewed
mlir/​lib/​Conversion/​AIRToAIESchedulingUtils.cpp Implements rendezvous lock allocation and selection. Critical concern: aggregate counting may allow repeated participation by one writer before others arrive, risking stale data.
mlir/​lib/​Conversion/​AIRToAIEPass.cpp Applies per-BD counts and ping-pong slot locks. Reviewed
mlir/​include/​air/​Conversion/​AIRToAIESchedulingUtils.h Defines the per-slot lock-set model and APIs. Reviewed

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +701 to +703
cls.cap_locks.push_back(
allocateLockOp(device, tile, /*init=*/cls.multiplicity()));
cls.sig_locks.push_back(allocateLockOp(device, tile, /*init=*/0));
@erwei-xilinx
erwei-xilinx marked this pull request as draft September 19, 2026 03:41
Four reflows reviewdog flagged, no semantic change.  My local check had run on
the wrong branch, so the CI failure was real.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants