File size: 52,550 Bytes
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90f3d2f
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0e45f
c855122
 
 
 
ce0e45f
 
c855122
 
 
 
 
 
 
 
ce0e45f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a827ebd
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0e45f
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0e45f
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0e45f
 
 
 
c855122
ce0e45f
 
c855122
 
90f3d2f
c855122
 
 
 
 
 
 
 
 
 
 
 
 
a827ebd
 
 
 
 
 
 
 
 
 
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a827ebd
c855122
 
 
a827ebd
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a827ebd
c855122
 
 
 
 
 
 
 
a827ebd
c855122
 
 
 
a827ebd
c855122
 
 
a827ebd
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0e45f
 
 
 
c855122
ce0e45f
 
c855122
 
 
 
 
 
 
 
 
 
a827ebd
c855122
a827ebd
 
 
 
 
 
 
 
c855122
 
90f3d2f
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
90f3d2f
c855122
90f3d2f
c855122
 
 
 
 
 
 
 
 
 
a827ebd
 
 
 
c855122
 
a827ebd
c855122
a827ebd
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6da9fa8
c855122
ce0e45f
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a827ebd
c855122
 
a827ebd
 
c855122
a827ebd
 
 
 
 
 
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a827ebd
c855122
a827ebd
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a827ebd
c855122
 
 
 
 
 
 
 
 
 
 
a827ebd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c855122
 
 
 
 
 
 
 
 
a827ebd
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a827ebd
c855122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ce0e45f
 
a827ebd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
"""K2 outer: fused QKV projection + 3D RoPE + vendor attention + fused out-projection, in ONE
top-level NKI kernel, FLATTENED from ``nki_attn_block_k2v_h10`` (which forks
``nki_attn_block_k2v_dp4``). Self-contained: imports only ``nki``, ``nkilib``, ``torch``, and
``_attention_cte`` from the sibling ``attention_inner`` -- NO imports from ``experiment/`` or
the repo-root incumbent modules.

WHAT THIS COMPUTES (per core; LNC=2, this core owns 8 of the 16 heads)
======================================================================
``result[LNC, N, H] bf16`` = out-projection PARTIALS of
``attention(rope(h@Wq), rope(h@Wk), h@Wv)``. Each core computes a partial sum over its 8 heads'
512 out-projection contraction rows; K3's residual add sums the two cores' partials.

Phase 1 (producer): dma_transpose h -> x_t; project Q/K (lane-permuted) and V; apply the six
rope ``tensor_tensor`` ops; scatter K into ``k_tiles`` (SBUF), Q into ``q_priv`` (private_hbm),
V into ``v_tiles`` (SBUF).
Phase 2 (attention + out-proj): reset the allocator to reclaim the dead producer buffers, call
``_attention_cte`` per local head into feature-major SBUF O slices, then contract the fused
out-projection (tokens on partitions) and DMA token-major to ``result``.

PSUM BANK PLAN (producer): V uses bank 7 (``(0, 7*_PSUM_BANK)``); Q/K parities use banks 5/6
(``(5+par)*_PSUM_BANK``); the fused out-projection uses bank 0 (``(0, 0)``). The vendor
attention (``attention_inner``) pins its own low banks {0-3} for MM1 and {4-7} for MM2 during
its phase, which runs after the producer's high-water mark, so there is no live overlap.

THE TWO ENGINE PINS (both to Scalar), and WHY (from PERF_INVENTORY)
-------------------------------------------------------------------
  * ``:276`` V-scatter ``tensor_copy`` -> SCALAR (H9's inherited mechanism): eight copies read
    one ``vout`` before the next vsub overwrites it (a WAR chain of 8); Scalar (240 ns) beats
    GpSimd (375 ns) for that chain and sheds the copy off Vector, K2's binding engine.
  * ``:458`` QK bias-add-and-evict ``tensor_scalar`` -> SCALAR (H10's mechanism): the largest
    remaining Vector-resident relocatable site (768/core); ``data=ps`` is PSUM so GpSimd is
    illegal, and Scalar's 612 ns fee is faster than the 744 ns Vector cell it replaces, so the
    zero-lead rope chain that consumes ``proj`` cannot lengthen.
The ``:224`` bias-cast ``tensor_copy`` and ``:483`` K-scatter ``tensor_copy`` stay UNPINNED
(compiler default) -- pinning them was dropped (:224 is 0.09% of the window; :483 has no
affordable landing engine), and they MUST stay unpinned to match the shipping stream.

HOST-SIDE FOLD ALGEBRA (packing helpers below, copied flat)
-----------------------------------------------------------
Affine-free LN1 folds gamma1/beta1 into the K2 weights at freeze time:
``pack_weights(wq*g1, wk*g1, bq+wq@t1, bk+wk@t1, ...)`` and ``pack_v_weights(wv*g1, bv+wv@t1)``.
The lane permutation puts each quad-of-heads' even/odd RoPE lanes in their own 128-partition
group so RoPE is six full-width ``tensor_tensor`` ops with no transposes; it cancels in
attention's Q/K contraction (a dot product is invariant under a shared permutation of the
contraction axis), so V is natural and nothing downstream un-permutes.
LNC2 BIAS HALVING: ``pack_out_weights`` halves the out-projection bias (``bo*0.5``) so the two
cores' partials sum to exactly one bias -- halving is bitwise exact in bf16 (it only decrements
the exponent, and summing two identical bf16 values is exact).

INFERENCE ONLY: no autograd; the packed weights are detached caches.

SBUF BUDGET (bytes/lane; budget = 192 KiB = 196608 B/lane), TWO GEOMETRIES
==========================================================================
N=2048 (16-frame clips) is the SHIPPING path: nothing below is gated on, so buffer shapes,
byte layout, DMA target addresses and fill VALUES are BIT-FOR-BIT the incumbent (verified:
torch.equal(perf_out.pt) == True, max abs diff 0.0). N=8192 (64-frame clips) is a SEPARATE,
gated path (``if N > 2048``) that the N<=2048 forward never enters.

                                   |  N=2048 (16f)  |  N=8192 (64f, gated)
    ------------------------------ + -------------- + --------------------------------------
    k_tiles (8 heads resident)     |     32768      |  0  (streamed to private_hbm)
    v_tiles (8 heads resident)     |     16384      |  0  (streamed to private_hbm)
    k_stage / v_stage (1 head)     |       -        |  16384 + 8192 = 24576 (one head at a time)
    x_t   (hidden^T ring)          |     32768      |  32768 (depth-4 token-block ring, was 131072)
    tab   (rope tables ring)       |     32768      |  32768 (depth-4 token-block ring, was 131072)
    producer working set           |    ~25000      |  ~25000
    ------------------------------ + -------------- + --------------------------------------
    PHASE-1 PEAK                   |    ~124 KiB    |  ~106 KiB   (FITS)
    ------------------------------ + -------------- + --------------------------------------
    phase2_base (persistent K/V)   |     49152      |  24576 (only k_stage/v_stage persist)
    wo_sb + bo_bc + o_feat + out   |     29696      |  78848
    vendor inner (per head)        |    ~45 KiB     |  ~251 KiB (single 8192 section, n_large=4:
                                   |                |   exp_tp_sb 131072 + mm1_masked 65536 +
                                   |                |   exp_sb 32768 balloon -- see below)
    ------------------------------ + -------------- + --------------------------------------
    PHASE-2 PEAK                   |    ~125 KiB    |  ~354912 B/lane  (DOES NOT FIT -- measured
                                   |                |   on device [NCC_INLA001] 128x354912, 1.81x
                                   |                |   the 196608 budget => N>2048 is REFUSED)

WHY N>2048 IS REFUSED. At N=8192 the shipping design has TWO independent SBUF overflows
(both confirmed on device as ``[NCC_INLA001] ... out of bound 128x526944``):
  (1) PHASE 1: the four N-scaled residents k_tiles(131072)+v_tiles(65536)+x_t(131072)+
      tab(131072) = 458752 B/lane coexist -- 2.3x the whole budget.
  (2) PHASE 2: eight heads' K/V resident (196608 B/lane) is the ENTIRE budget.
The phase-1 fixes work (x_t/tab depth-4 token-block rings, bit-for-bit at N<=2048, saving
196608 B/lane; K/V streamed through private_hbm one head at a time) => phase-1 peak ~106 KiB.
But PHASE 2 does NOT fit: handing one head all 8192 keys makes the single-section flatten run
with num_large_tiles_per_section=ceil(8192/2048)=4, ballooning three inner buffers (exp_tp_sb
131072 + mm1_masked 65536 + exp_sb 32768 = 229376 B/lane) -> phase-2 peak measured 354912 B/lane,
1.81x the budget. The shrinkable phase-2 region (o_feat/out/wo/bo = 78848) is smaller than the
158304 overflow, so streaming O or shrinking o_feat cannot close the gap; re-enabling flash
sectioning fits but is numerically wrong (rel_l2 0.90, the V-JEPA2 kernels' verification notes), and
editing the inner would perturb the bit-for-bit 16-frame path. So the host wrapper
``attention_block`` raises ``NotImplementedError`` for N>2048 rather than emit a non-fitting or
wrong kernel (see the V-JEPA2 kernels' verification notes). The N<=2048 path is bit-for-bit unchanged.
"""
from __future__ import annotations

import torch
from nki.framework.kernel import wrap_nki_to_torch_hop
from torch_neuronx import nki_op

import nki
import nki.isa as nisa
import nki.language as nl
import os as _osr
_ROPE_BF16 = _osr.environ.get("VIT_ENC_ROPE_BF16", "0") == "1"
_ROPE_DT = nl.bfloat16 if _ROPE_BF16 else nl.float32
from nki.isa import engine

from nkilib.core.utils.modular_allocator import ModularAllocator

# The vendor attention, flattened to this model's reached path with the h8/h10 pins inlined.
# The N<=2048 path calls THIS single-section flatten; the N>2048 path is REFUSED (see below).
# seqlen_k=8192 < the vendor's _FLASH_ATTENTION_THRESHOLD (10240), so num_sections==1 -- the
# SAME single-section, non-flash code path the verified 16-frame configuration uses (no
# multi-section online-softmax combine, which is numerically wrong here: the V-JEPA2 kernels' verification notes).
# BUT single-section at 8192 does NOT fit: the inner's mm1_masked / exp_sb / exp_tp_sb are
# allocated with num_free_tiles carrying num_large_tiles_per_section = ceil(8192/2048) = 4
# (attention_inner.py:658-701), so those three buffers BALLOON with total_seqlen_k -- they are
# NOT reused across the large-tile loop. exp_tp_sb 131072 + mm1_masked 65536 + exp_sb 32768 =
# 229376 B/lane, driving phase-2 peak to ~354912 B/lane (1.81x the 196608 budget; measured on
# device [NCC_INLA001] 128x354912). See the SBUF table and refusal rationale below.
from .attention_inner import _attention_cte

import os as _os
#: logical-core degree the kernels are built for; must match NEURON_LOGICAL_NC_CONFIG.
LNC = int(_os.environ.get("NEURON_LOGICAL_NC_CONFIG", "2"))
_SPLIT = 2  # heads are always split in two halves (8 heads per program)
_P_MAX = 128
_PSUM_FMAX = 512
_PSUM_BANK = 2048          # bytes per partition per bank
_K_TILE = 512              # vendor k_cache_sbuf tiles are (d, 512)
_V_TILE = 128              # vendor v_cache_sbuf tiles are (128, d)


def _kernel_assert(cond: bool, msg: str):
    assert cond, f"[INTERNAL_ERROR] [NCC_INKI016] Kernel validation exception: {msg}"


# ==========================================================================================
# Host-side packing helpers (copied flat from nki_qk_fused_2core / nki_attn_block_k2v_dp4).
# All are frozen-weight work, cached once; INFERENCE ONLY.
# ==========================================================================================
def lane_permutation(heads: int, D: int, rope: str = "half") -> torch.Tensor:
    """Column order that puts each quad-of-heads' RoPE pair halves in their own 128-row group.

    RoPE rotates pairs of lanes ``(a, b)`` of each head. ``rope`` picks the pairing:
      * ``"half"``        -- (j, j + D/2), the rotate-half convention (DINOv3, EVA-02, HF Llama style).
      * ``"interleaved"`` -- (2j, 2j + 1), adjacent lanes (V-JEPA2 / GPT-NeoX-interleaved style).
    The projection's output columns are reordered so group ``2a`` holds the FIRST lane of every
    pair for heads ``4a..4a+3`` (at D=64; 2 groups per 4 heads at D=128) and group ``2a+1`` the
    SECOND lane, so RoPE is six full-width elementwise ops with no transposes. The permutation
    cancels in the Q/K contraction, so nothing downstream un-permutes.

    Returns ``perm`` of length ``heads*D`` s.t. ``w.t()[:, perm]`` is the reordered weight.
    """
    _kernel_assert(rope in ("half", "interleaved"), f"rope must be 'half' or 'interleaved', got {rope!r}")
    half = D // 2
    quad = _P_MAX // half
    _kernel_assert(quad * half == _P_MAX, f"D={D} gives half={half}, needs quad*half=128")
    _kernel_assert(heads % quad == 0, f"heads={heads} must be divisible by quad={quad}")
    perm = torch.empty(heads * D, dtype=torch.long)
    for hd in range(heads):
        a, b = hd // quad, hd % quad
        for j in range(half):
            lo, hi = (j, half + j) if rope == "half" else (2 * j, 2 * j + 1)
            perm[(2 * a) * _P_MAX + b * half + j] = hd * D + lo
            perm[(2 * a + 1) * _P_MAX + b * half + j] = hd * D + hi
    return perm


def pack_weights(wq: torch.Tensor, wk: torch.Tensor,
                 bq: torch.Tensor | None, bk: torch.Tensor | None,
                 heads: int, D: int, rope: str = "half"):
    """``(w_packed [2,H,H] bf16, b_packed [2,128,H/128] fp32)`` -- transposed and column-permuted.

    FOLD: in the affine-free-LN1 freeze this is called on gamma-folded inputs
    ``pack_weights(wq*g1, wk*g1, bq+wq@t1, bk+wk@t1, heads, D)``. ``wq``/``wk`` are torch
    ``Linear.weight`` ``[H_out, H_in]``; the kernel wants ``[H_in, H_out]`` with ``H_out``
    permuted, i.e. ``w.t()[:, perm]``. The bias is packed by :func:`pack_qk_bias` so each program
    loads all of its 128-row groups in one DMA per q/k.
    """
    H = wq.shape[0]
    perm = lane_permutation(heads, D, rope).to(wq.device)
    w = torch.stack([wq.detach().t()[:, perm].contiguous(),
                     wk.detach().t()[:, perm].contiguous()], 0)
    zero = torch.zeros(H, dtype=wq.dtype, device=wq.device)
    b = torch.stack([(zero if bq is None else bq.detach())[perm].contiguous(),
                     (zero if bk is None else bk.detach())[perm].contiguous()], 0)
    return w.to(torch.bfloat16).contiguous(), pack_qk_bias(b.to(torch.bfloat16).float())


def pack_qk_bias(b: torch.Tensor) -> torch.Tensor:
    """``[2, Fp]`` (or legacy ``[2, Fp, 1]``) permuted Q/K bias -> ``[2, 128, Fp/128]`` fp32.

    ``out[qk, p, g] == b[qk, g*128 + p]``: column ``g`` is the per-partition bias of 128-row group
    ``g``, so a program loads all of its groups with one DMA per q/k. Values are first rounded to
    bf16 (the precision the v1 kernel loaded and cast), which keeps the output bit-identical to v1.
    """
    if b.dim() == 3 and b.shape[1] == _P_MAX:
        return b.float().contiguous()
    if b.dim() == 3:
        b = b.squeeze(-1)
    Fp = b.shape[1]
    _kernel_assert(Fp % _P_MAX == 0, f"bias length {Fp} must be a multiple of 128")
    return b.to(torch.bfloat16).float().reshape(2, Fp // _P_MAX, _P_MAX).transpose(1, 2).contiguous()


def pack_tables(cos: torch.Tensor, sin: torch.Tensor, heads: int, D: int, rope: str = "half"):
    """``tab [4, 128, N] fp32`` RoPE tables, quad-tiled to match ``lane_permutation(..., rope)``.

    Inputs are the model's own ``[N, D]`` cos / sin tables (shared by every head; prefix tokens that
    take no rotation: cos=1, sin=0). With ``(a, b)`` a lane pair the kernel computes
    ``a' = a*tab0 + b*tab2`` and ``b' = b*tab1 + a*tab3``, so:
      * ``"half"``:        ``sin`` is the PLAIN sin table (rotate-half: ``x*cos + rotate_half(x)*sin``);
                           tab = (cos_lo, cos_hi, -sin_lo, +sin_hi).
      * ``"interleaved"``: ``sin`` is the SIGNED sin table (``-sin`` on even lanes, ``+sin`` on odd);
                           tab = (cos_even, cos_odd, sinsigned_even, sinsigned_odd).
    """
    _kernel_assert(rope in ("half", "interleaved"), f"rope must be 'half' or 'interleaved', got {rope!r}")
    half = D // 2
    quad = _P_MAX // half
    if rope == "half":
        parts = [cos[:, :half], cos[:, half:], -sin[:, :half], sin[:, half:]]
    else:
        parts = [cos[:, 0::2], cos[:, 1::2], sin[:, 0::2], sin[:, 1::2]]
    tab = torch.stack([p.t().contiguous().repeat(quad, 1) for p in parts], 0)
    return tab.to(torch.float32).contiguous()


def pack_v_weights(wv: torch.Tensor, bv: torch.Tensor | None):
    """``(wv_t [H, H] bf16, bv [1, H] bf16)``. V is NOT lane-permuted (natural order).

    FOLD: in the affine-free-LN1 freeze this is called on gamma-folded inputs
    ``pack_v_weights(wv*g1, bv+wv@t1)``.
    """
    H = wv.shape[0]
    zero = torch.zeros(H, dtype=wv.dtype, device=wv.device)
    return (wv.detach().t().contiguous().to(torch.bfloat16),
            (bv.detach() if bv is not None else zero).reshape(1, H).contiguous().to(torch.bfloat16))


def pack_out_weights(wo, bo):
    """``(wo_t [H, H] bf16, bo/2 [1, H] bf16)`` for the fused out projection.

    ``wo`` is ``attn.proj.weight`` [H, H] (attention features -> hidden). The kernel contracts
    over attention features, so it wants those on partitions: ``wo.T`` = [features, hidden].

    LNC2 BIAS HALVING. Both cores add ``bo/2`` and K3's partial sum restores ``bo``. Adding the
    full bias on both would double it; adding it on one core only would need ``program_id`` as an
    arithmetic operand (it is symbolic -- neither a compile-time constant nor a [P,1] tile).
    Halving is bitwise exact in bf16: it only decrements the exponent, and summing two identical
    bf16 values is exact.
    """
    import torch as _t
    Hh = wo.shape[0]
    zero = _t.zeros(Hh, dtype=wo.dtype, device=wo.device)
    half = (bo.detach() if bo is not None else zero) * 0.5
    return (wo.detach().t().contiguous().to(_t.bfloat16),
            half.reshape(1, Hh).contiguous().to(_t.bfloat16))


# ==========================================================================================
# THE KERNEL. Flattened from _attn_block_k2v_h10_dp_kernel (verbatim dp4 body with the two
# engine= keywords hardcoded to engine.scalar at :276 and :458; :224 and :483 left unpinned).
# ==========================================================================================
@nki.jit
def _attention_block_kernel(h_hbm, wqk_hbm, bqk_hbm, tab_hbm, wvt_hbm, bv_hbm,
                            wot_hbm, bo_hbm, scale, head_dim, n_valid, hb, grid, nimg=1, resident=0, pack2=0):
    """out[16, N, D] bf16 = attention(rope(h@Wq), rope(h@Wk), h@Wv), one kernel (per-core partials).

    BATCHED (nimg>1): h_hbm is the STACKED [nimg*N, H] buffer (images back to back, N rows each).
    The per-image body below runs once per image inside this ONE launch, reading image b's rows at
    row offset b*N and writing partials to result[slot, b*N:(b+1)*N]. ONE launch, no host-side
    slice/cat glue.
    resident=0: the loop wraps the whole body, so weights/biases/rope tables are re-DMA'd per image.
    resident=1 (N<=2048): Q/K/V/O weights, biases and rope tables are staged ONCE at the bottom of
    SBUF before the image loop (~38 KiB/lane ViT-L + tables); each image's buffers are allocated
    above them. Same math, bit-identical outputs."""
    NT, H = h_hbm.shape
    N = NT // nimg
    Fp = wqk_hbm.shape[2]                 # padded attention features = 512 * n_groups
    D = head_dim
    half = D // 2
    quad = _P_MAX // half                 # heads per 128-partition parity group = 4
    heads = Fp // D                       # padded head count (multiple of 8)
    local_heads = _PSUM_FMAX // D         # heads per program: 8 at D=64, 4 at D=128
    local_quads = local_heads // quad     # 2
    n_chunks = H // _P_MAX                # 8 contraction chunks
    n_tt = (N + _PSUM_FMAX - 1) // _PSUM_FMAX   # token tiles of 512 (last may be ragged)
    n_ktiles = (N + _K_TILE - 1) // _K_TILE     # K tiles / head (last may be partially filled)
    n_vtiles = N // _V_TILE               # 16 V tiles / head
    n_ochunks = (local_heads * D) // _P_MAX   # feature chunks of the out projection
    v_per_tt = _PSUM_FMAX // _V_TILE      # 4 V tiles per 512-token tile

    # N>2048 gate (see the phase-1/phase-2 note below). Computed early so the K/V residents can
    # be allocated as HBM (streamed) for the big path.
    _big = N > _PSUM_FMAX * 4

    # N>2048 IS NOT SUPPORTED (guarded at the host wrapper `attention_block` below, which raises
    # NotImplementedError before this kernel is ever traced -- NKI kernels cannot `raise`). The
    # phase-1 memory fixes (x_t/tab depth-4 rings, K/V streamed through private_hbm one head at a
    # time) DO fit -- phase-1 peak ~106 KiB/lane. But phase 2 does NOT: handing one head all 8192
    # keys makes the flattened single-section _attention_cte run with num_large_tiles_per_section
    # =ceil(8192/2048)=4, which balloons three N-scaled inner buffers -- exp_tp_sb (8*4*4*512*2 =
    # 131072), mm1_masked (2*4*2048*4 = 65536), exp_sb (2*4*2048*2 = 32768) = 229376 B/lane for
    # three buffers alone. Measured on device (2026-09-25, neuronx-cc 2.25): [NCC_INLA001] out of
    # bound 128x354912 -> phase-2 peak 354912 B/lane, 1.81x the 196608 budget. The shrinkable
    # phase-2 region (o_feat 65536 + out 1024 + wo/bo 12288 = 78848) is smaller than the 158304
    # overflow, so streaming O or shrinking o_feat CANNOT close the gap -- the inner's N-scaled
    # buffers are the wall. Re-enabling flash sectioning (2048-key sections) shrinks the inner but
    # activates the vendor's multi-section online-softmax combine, a path the shipping config never
    # exercises and which produced rel_l2 0.90 (wrong). See the V-JEPA2 kernels' verification notes.

    _kernel_assert(wqk_hbm.shape == (2, H, Fp), f"wqk must be [2,{H},{Fp}]")
    _kernel_assert(bqk_hbm.shape == (2, _P_MAX, Fp // _P_MAX), f"bqk must be pack_qk_bias(...): [2,128,{Fp // _P_MAX}]")
    _kernel_assert(tab_hbm.shape == (4, _P_MAX, N), f"tab must be [4,{_P_MAX},{N}]")
    _kernel_assert(wvt_hbm.shape == (H, Fp) and bv_hbm.shape == (1, Fp), "bad V weights")
    _kernel_assert(wot_hbm.shape == (Fp, H) and bo_hbm.shape == (1, H), "bad O weights")
    _kernel_assert(H % _P_MAX == 0, f"H={H} must be a multiple of 128")
    _kernel_assert(quad * half == _P_MAX, f"D={D} must give 4 heads per group")
    _kernel_assert(Fp % _PSUM_FMAX == 0, "padded features must be a multiple of 512")
    _kernel_assert(local_heads * D == _PSUM_FMAX,
                   "inline V projection wants all local heads in one 512-wide stream")
    _kernel_assert(N % _P_MAX == 0, f"N={N} must be a multiple of 128")
    _kernel_assert(NT == nimg * N, f"rows {NT} must be nimg({nimg}) x N")
    _kernel_assert(str(h_hbm.dtype) == str(nl.bfloat16), "h must be bf16")

    # [LNC, N, H] PARTIALS. The out projection contracts over all H features == all 16 heads,
    # and this core owns 8, so what it computes is a partial sum; K3 adds the two.
    result = nl.ndarray((grid, NT, H), dtype=nl.bfloat16, buffer=nl.shared_hbm)

    # Each program handles one 8-head group: group = hb + program_id. grid==1 -> single program.
    if grid == 1:
        pid = hb
        out_slot = 0
    else:
        out_slot = nl.program_id(0)       # SYMBOLIC: index arithmetic only
        pid = hb + out_slot
    head0 = pid * local_heads
    col0 = pid * (local_heads * D)        # first permuted qk weight column
    vcol0 = pid * (local_heads * D)       # first natural v weight column (same offset)
    gcol0 = pid * ((local_heads * D) // _P_MAX)   # first 128-row bias group (multiply only: pid may be symbolic)

    # ---- RESIDENT MODE (resident=1, N<=2048 only): stage the per-launch-invariant inputs ONCE,
    # at the BOTTOM of SBUF, before the image loop. Everything per image is then allocated above
    # `res_top`. Invariant: Q/K/V/O weights, their biases, and the rope tables (same for every image).
    # Cost ~38 KiB/lane (ViT-L) + tables; per-image phase peaks were <=106 KiB, budget 192 KiB.
    _res = bool(resident) and not (N > _PSUM_FMAX * 4)
    res_top = 0
    if _res:
        ra = ModularAllocator(initial_address=0)
        r_w_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
                                      block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
        for qk in range(2):
            for c in range(n_chunks):
                nisa.dma_copy(dst=r_w_sb[qk][c],
                              src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX, col0:col0 + local_quads * 2 * _P_MAX])
        r_wv_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
                                       block_dim=[n_chunks], num_free_tiles=[n_chunks])
        for c in range(n_chunks):
            nisa.dma_copy(dst=r_wv_sb[c], src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX, vcol0:vcol0 + local_heads * D])
        # Q/K biases: host-packed fp32 [2, 128, Fp/128] (pack_qk_bias); this program's groups land in
        # ONE DMA per q/k on the Scalar HW-DGE queue (was one [128,1] DMA + one cast per group).
        r_b_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2), dtype=nl.float32,
            block_dim=[2], num_free_tiles=[2], align_to=4)
        for qk in range(2):
            nisa.dma_copy(dst=r_b_sb[qk], src=bqk_hbm[qk, 0:_P_MAX, gcol0:gcol0 + local_quads * 2],
                          dge_mode=nisa.dge_mode.hwdge, engine=nisa.engine.scalar)
        r_bv_bc = ra.alloc_sbuf_tensor(shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
        nisa.dma_copy(dst=r_bv_bc, src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]], offset=vcol0))
        r_tab = ra.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=_ROPE_DT,
                                     block_dim=[4, n_tt], num_free_tiles=[4, n_tt], align_to=4)
        for s in range(n_tt):
            for t_i in range(4):
                _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
                nisa.dma_copy(dst=r_tab[t_i][s][:, 0:_tw], src=tab_hbm[t_i, 0:_P_MAX, s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])
        r_wo_sb = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.bfloat16,
                                       block_dim=[n_ochunks], num_free_tiles=[n_ochunks])
        for oc in range(n_ochunks):
            nisa.dma_copy(dst=r_wo_sb[oc], src=wot_hbm[col0 + oc * _P_MAX:col0 + (oc + 1) * _P_MAX, 0:H])
        r_bo_bc = ra.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
        nisa.dma_copy(dst=r_bo_bc, src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))
        res_top = ra.get_current_address()

    # v1.6 (pack2): two images share one producer pass when 2N <= 512 (e.g. 224 px): their rows are contiguous,
    # so one 512-token x^T tile holds both and the Q/K/V projections + RoPE stream 2N columns per matmul.
    # Attention runs per image (separate _attention_cte calls on per-image K/V/Q slices); the out-projection
    # covers both images' rows. Same arithmetic per element -> bit-identical to the unpacked kernel.
    PK = 2 if (pack2 and nimg % 2 == 0 and 2 * N <= _PSUM_FMAX) else 1
    NP = N * PK
    n_ttp = (NP + _PSUM_FMAX - 1) // _PSUM_FMAX
    n_ktp = (NP + _K_TILE - 1) // _K_TILE
    n_vtp = NP // _V_TILE
    n_grp1 = N // _P_MAX
    for img in range(nimg // PK):        # unrolled at trace time (python int)
        rofs = img * NP * H               # element offset of this image (pair)'s rows in h / result
        allocator = ModularAllocator(initial_address=res_top)

        # ---- persistent consumer tiles (written by producer, read by attention) ---------
        # N<=2048: all 8 heads' K/V resident in SBUF (bit-for-bit shipping path). N>2048: K/V for
        # all 8 heads (196608 B/lane) is the ENTIRE 192 KiB budget, so they cannot stay resident
        # alongside x_t/tab/working set. STREAM them: the producer scatters each head's K/V to
        # private_hbm, and phase-2 loads ONE head's K/V into small resident staging rings just
        # before its attention call (attention runs per head and writes O to HBM, so only one
        # head's K/V need be live at a time).
        k_tiles = []
        v_tiles = []
        k_hbm = []
        v_hbm = []
        if _big:
            for _lh in range(local_heads):
                k_hbm.append(nl.ndarray((n_ktiles, D, _K_TILE), dtype=nl.bfloat16,
                                        buffer=nl.private_hbm))
                v_hbm.append(nl.ndarray((n_vtiles, _V_TILE, D), dtype=nl.bfloat16,
                                        buffer=nl.private_hbm))
            # ONE head's resident staging rings (reused across heads in phase 2).
            k_stage = allocator.alloc_sbuf_tensor(
                shape=(D, _K_TILE), dtype=nl.bfloat16,
                block_dim=[n_ktiles], num_free_tiles=[n_ktiles], align_to=32)
            v_stage = allocator.alloc_sbuf_tensor(
                shape=(_V_TILE, D), dtype=nl.bfloat16,
                block_dim=[n_vtiles], num_free_tiles=[n_vtiles])
        else:
            for _lh in range(local_heads):
                kt_ = allocator.alloc_sbuf_tensor(
                    shape=(D, _K_TILE), dtype=nl.bfloat16,
                    block_dim=[n_ktp], num_free_tiles=[n_ktp], align_to=32)
                k_tiles.append(kt_)
                vt_ = allocator.alloc_sbuf_tensor(
                    shape=(_V_TILE, D), dtype=nl.bfloat16,
                    block_dim=[n_vtp], num_free_tiles=[n_vtp])
                v_tiles.append(vt_)
        # HIGH WATER MARK for phase 2: k_tiles/v_tiles are the only phase-1 products attention
        # needs, so every buffer above this address is dead once the producer loop finishes.
        phase2_base = allocator.get_current_address()

        # ------------------------------------------------------------------------------------
        # N>2048 GATE (64-frame clips, N=8192). At N<=2048 every ring below keeps its full-N
        # depth, so the buffer shape, byte layout, DMA target addresses and fill VALUES are
        # bit-for-bit identical to the shipping 16-frame path (see the SBUF budget table in the
        # module docstring), and the attention call goes to the pinned single-section flatten
        # ``_attention_cte`` (all engine pins intact). At N>2048 two independent overflows appear:
        #   (1) PHASE 1: the four N-scaled residents (k_tiles+v_tiles+x_t+tab = 458752 B/lane)
        #       coexist. FIX: x_t and tab become depth-XT_RING token-block rings (a rolling 4-tile
        #       window filled inside the per-token-tile loop; a pure copy, identical values).
        #   (2) PHASE 2: eight heads' K/V resident (196608 B/lane) is the whole budget. FIX: stream
        #       K/V through private_hbm and stage ONE head at a time into k_stage/v_stage. The
        #       attention call is the SAME single-section flatten ``_attention_cte`` the N<=2048 path
        #       uses (seqlen_k=8192 < 10240 threshold => num_sections==1, no multi-section flash
        #       combine); its large-tile loop iterates 4x over _LARGE_TILE_SZ-sized buffers.
        # The N<=2048 path enters NEITHER branch and is bit-for-bit unchanged.
        _big = N > _PSUM_FMAX * 4                 # N>2048  -> the 64-frame geometry
        XT_RING = min(n_tt, 4) if _big else n_ttp  # token-block ring depth for x_t / tab

        # ---- resident producer inputs ----------------------------------------------------
        # x^T: hidden on partitions, tokens on the free axis, as a per-512-token-block ring.
        # At N<=2048 XT_RING==n_tt so the ring holds every block (== the old (128,N) buffer,
        # same bytes, same addresses); at N=8192 it holds a rolling window of 4 blocks. The fill
        # is a pure dma_transpose (no arithmetic), so any fill ORDER yields identical values.
        x_t = allocator.alloc_sbuf_tensor(
            shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16,
            block_dim=[n_chunks, n_ttp], num_free_tiles=[n_chunks, XT_RING], align_to=32)

        if not _big:
            # N<=2048: fill the whole buffer up front, exactly as the shipping path did (pure
            # dma_transpose; block-indexed AP lands at the same addresses as the old (128,N) buffer).
            for s in range(n_ttp):
                for c in range(n_chunks):
                    nisa.dma_transpose(
                        dst=x_t[c][s].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1],
                                          [1, min(_PSUM_FMAX, NP - s * _PSUM_FMAX)]], offset=0),
                        src=h_hbm.ap([[H, min(_PSUM_FMAX, NP - s * _PSUM_FMAX)], [1, 1], [1, 1], [1, _P_MAX]],
                                     offset=rofs + (s * _PSUM_FMAX) * H + c * _P_MAX),
                    )

        if _res:
            w_sb, wv_sb, b_sb, bv_bc = r_w_sb, r_wv_sb, r_b_sb, r_bv_bc
        else:
            # this core's Q/K weight columns (lane-permuted) and V columns (natural)
            w_sb = allocator.alloc_sbuf_tensor(
                shape=(_P_MAX, local_quads * 2 * _P_MAX), dtype=nl.bfloat16,
                block_dim=[2, n_chunks], num_free_tiles=[2, n_chunks])
            for qk in range(2):
                for c in range(n_chunks):
                    nisa.dma_copy(dst=w_sb[qk][c],
                                  src=wqk_hbm[qk, c * _P_MAX:(c + 1) * _P_MAX,
                                              col0:col0 + local_quads * 2 * _P_MAX])
            wv_sb = allocator.alloc_sbuf_tensor(
                shape=(_P_MAX, local_heads * D), dtype=nl.bfloat16,
                block_dim=[n_chunks], num_free_tiles=[n_chunks])
            for c in range(n_chunks):
                nisa.dma_copy(dst=wv_sb[c],
                              src=wvt_hbm[c * _P_MAX:(c + 1) * _P_MAX,
                                          vcol0:vcol0 + local_heads * D])

            # biases: qk as (128,1) per group; v broadcast to (128, 8*64) once
            # Q/K biases: host-packed fp32 [2, 128, Fp/128] (pack_qk_bias); this program's groups land in
            # ONE DMA per q/k on the Scalar HW-DGE queue (was one [128,1] DMA + one cast per group).
            b_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, local_quads * 2), dtype=nl.float32,
                block_dim=[2], num_free_tiles=[2], align_to=4)
            for qk in range(2):
                nisa.dma_copy(dst=b_sb[qk], src=bqk_hbm[qk, 0:_P_MAX, gcol0:gcol0 + local_quads * 2],
                              dge_mode=nisa.dge_mode.hwdge, engine=nisa.engine.scalar)
            bv_bc = allocator.alloc_sbuf_tensor(
                shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)
            nisa.dma_copy(dst=bv_bc,
                          src=bv_hbm.ap(pattern=[[0, _V_TILE], [1, local_heads * D]],
                                        offset=vcol0))

        # rope tables, resident (quad-tiled on the host: identical rows for every quad). Token-block
        # ring, same gating as x_t: at N<=2048 TAB_RING==n_tt holds every block (== old (128,N)
        # buffer, bit-for-bit); at N=8192 a rolling 4-block window filled inside the tt loop. Pure
        # dma_copy -> identical values regardless of fill order.
        if _res and PK == 1:
            tab = r_tab
        elif PK == 2:
            # pack2: the same per-image tables side by side in one 512-wide tile
            tab = allocator.alloc_sbuf_tensor(
                shape=(_P_MAX, _PSUM_FMAX), dtype=_ROPE_DT,
                block_dim=[4, 1], num_free_tiles=[4, 1], align_to=4)
            for t_i in range(4):
                for sub in range(2):
                    nisa.dma_copy(dst=tab[t_i][0][:, sub * N:(sub + 1) * N], src=tab_hbm[t_i, 0:_P_MAX, 0:N])
        else:
            tab = allocator.alloc_sbuf_tensor(
                shape=(_P_MAX, _PSUM_FMAX), dtype=_ROPE_DT,
                block_dim=[4, n_tt], num_free_tiles=[4, XT_RING], align_to=4)

            if not _big:
                for s in range(n_tt):
                    for t_i in range(4):
                        _tw = min(_PSUM_FMAX, N - s * _PSUM_FMAX)
                        nisa.dma_copy(dst=tab[t_i][s][:, 0:_tw],
                                      src=tab_hbm[t_i, 0:_P_MAX,
                                                  s * _PSUM_FMAX:s * _PSUM_FMAX + _tw])

        # ---- per-tile working set (reused across iterations; same tensor objects) --------
        proj = allocator.alloc_sbuf_tensor(
            shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, block_dim=[2],
            num_free_tiles=[2])
        acc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=_ROPE_DT,
                                          align_to=4)
        tmp = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX), dtype=_ROPE_DT,
                                          align_to=4)
        roped = allocator.alloc_sbuf_tensor(
            shape=(_P_MAX, _PSUM_FMAX), dtype=nl.bfloat16, block_dim=[2],
            num_free_tiles=[2])
        vout = allocator.alloc_sbuf_tensor(
            shape=(_V_TILE, local_heads * D), dtype=nl.float32, align_to=4)

        # Q staging via private_hbm, one per LOCAL head (vendor segmented-kernel pattern)
        q_priv = []
        for _lh in range(local_heads):
            qs_ = []
            for _sub in range(PK):
                qs_.append(nl.ndarray((1, D, N), dtype=nl.bfloat16, buffer=nl.private_hbm))
            q_priv.append(qs_)

        # ---- producer: per 512-token tile, straight through the pipeline ----------------
        for tt in range(n_ttp):
            t0 = tt * _PSUM_FMAX
            tw = min(_PSUM_FMAX, NP - t0)         # ragged last token tile (N % 512 != 0)

            # N>2048: fill this token-block's x_t / tab slice NOW (rolling ring window). At
            # N<=2048 the whole buffer was already filled up front (bit-for-bit), so skip.
            if _big:
                for c in range(n_chunks):
                    nisa.dma_transpose(
                        dst=x_t[c][tt].ap([[_PSUM_FMAX, _P_MAX], [1, 1], [1, 1], [1, _PSUM_FMAX]],
                                          offset=0),
                        src=h_hbm.ap([[H, _PSUM_FMAX], [1, 1], [1, 1], [1, _P_MAX]],
                                     offset=rofs + t0 * H + c * _P_MAX),
                    )
                for t_i in range(4):
                    nisa.dma_copy(dst=tab[t_i][tt],
                                  src=tab_hbm[t_i, 0:_P_MAX, t0:t0 + _PSUM_FMAX])

            # V for all 8 local heads of this token tile.
            for vsub in range(tw // _V_TILE):
                ps_v = nl.ndarray((_V_TILE, local_heads * D), dtype=nl.float32,
                                  buffer=nl.psum, address=(0, 7 * _PSUM_BANK))
                for c in range(n_chunks):
                    nisa.nc_matmul(
                        dst=ps_v,
                        stationary=x_t[c][tt][:, vsub * _V_TILE:(vsub + 1) * _V_TILE],
                        moving=wv_sb[c],
                        accumulate=(c > 0),
                    )
                nisa.tensor_tensor(dst=vout, data1=ps_v, data2=bv_bc, op=nl.add)
                for lh in range(local_heads):
                    if _big:
                        # N>2048: stream V to HBM (SBUF->HBM dma from vout directly). The pinned
                        # :276 tensor_copy is the N<=2048 shipping site and is left untouched below.
                        nisa.dma_copy(
                            dst=v_hbm[lh][tt * v_per_tt + vsub],
                            src=vout[:, lh * D:(lh + 1) * D])
                        continue
                    # ======== SITE :276 -- pinned to Scalar. WAR chain of 8 copies read one vout
                    # before the next vsub overwrites it; Scalar (240 ns) beats GpSimd (375 ns) and
                    # sheds the copy off Vector, the binding engine. ========
                    nisa.tensor_copy(
                        dst=v_tiles[lh][tt * v_per_tt + vsub],
                        src=vout[:, lh * D:(lh + 1) * D],
                        engine=engine.scalar)

            # Q and K for both local quads, both parities, this token tile.
            for qk in range(2):
                for j in range(local_quads):
                    for par in range(2):
                        g = j * 2 + par
                        cols = g * _P_MAX
                        ps = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32,
                                        buffer=nl.psum,
                                        address=(0, (5 + par) * _PSUM_BANK))
                        for c in range(n_chunks):
                            nisa.nc_matmul(
                                dst=ps[:, 0:tw],
                                stationary=w_sb[qk][c][:, cols:cols + _P_MAX],
                                moving=x_t[c][tt][:, 0:tw],
                                accumulate=(c > 0),
                            )
                        # ======== SITE :458 -- pinned to Scalar. Largest remaining Vector-resident
                        # relocatable site (768/core); data=ps is PSUM so GpSimd is illegal, and
                        # Scalar's 612 ns fee is faster than the 744 ns Vector cell, so the
                        # zero-lead rope chain consuming proj cannot lengthen. ========
                        # NKI 0.7: a Scalar-engine pin here fails ISA opcode_engine_check on newer compilers
                        # newer compilers -> unpinned (compiler picks the engine).
                        nisa.tensor_scalar(dst=proj[par][:, 0:tw], data=ps[:, 0:tw],
                                           op0=nl.add, operand0=b_sb[qk][:, g:g + 1])

                    # rope: the six ops of nki_rope, same order, tables sliced per tile
                    nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[0][:, 0:tw],
                                       data2=tab[0][tt][:, 0:tw], op=nl.multiply)
                    nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[1][:, 0:tw],
                                       data2=tab[2][tt][:, 0:tw], op=nl.multiply)
                    nisa.tensor_tensor(dst=roped[0][:, 0:tw], data1=acc[:, 0:tw], data2=tmp[:, 0:tw], op=nl.add)
                    nisa.tensor_tensor(dst=acc[:, 0:tw], data1=proj[1][:, 0:tw],
                                       data2=tab[1][tt][:, 0:tw], op=nl.multiply)
                    nisa.tensor_tensor(dst=tmp[:, 0:tw], data1=proj[0][:, 0:tw],
                                       data2=tab[3][tt][:, 0:tw], op=nl.multiply)
                    nisa.tensor_tensor(dst=roped[1][:, 0:tw], data1=acc[:, 0:tw], data2=tmp[:, 0:tw], op=nl.add)

                    # scatter: head b of the quad owns rows [b*32,(b+1)*32)
                    for b in range(quad):
                        lh = j * quad + b
                        r0 = b * half
                        if qk == 1:
                            for par2 in range(2):
                                if _big:
                                    # N>2048: stream K to HBM (SBUF->HBM dma). The :483 tensor_copy
                                    # below is the N<=2048 shipping site, left untouched.
                                    nisa.dma_copy(
                                        dst=k_hbm[lh][tt][par2 * half:(par2 + 1) * half,
                                                          0:_K_TILE],
                                        src=roped[par2][r0:r0 + half, 0:_PSUM_FMAX])
                                    continue
                                # SITE :483 -- K-scatter COPY, left UNPINNED (no affordable landing
                                # engine: it is a WAR chain against the next iteration's rope write).
                                nisa.tensor_copy(
                                    dst=k_tiles[lh][tt][par2 * half:(par2 + 1) * half,
                                                        0:tw],
                                    src=roped[par2][r0:r0 + half, 0:tw])
                        elif PK == 1:
                            for par2 in range(2):
                                nisa.dma_copy(
                                    dst=q_priv[lh][0][0, par2 * half:(par2 + 1) * half,
                                                      t0:t0 + tw],
                                    src=roped[par2][r0:r0 + half, 0:tw])
                        else:
                            for par2 in range(2):
                                for sub in range(2):
                                    nisa.dma_copy(
                                        dst=q_priv[lh][sub][0, par2 * half:(par2 + 1) * half, 0:N],
                                        src=roped[par2][r0:r0 + half, sub * N:(sub + 1) * N])

        # ---- PHASE 2: reclaim the dead producer buffers. NO ALIASING. -------------------
        allocator.set_current_address(phase2_base)

        if _res:
            wo_sb, bo_bc = r_wo_sb, r_bo_bc
        else:
            # ---- out-projection weights: THIS core's feature rows, all H output columns -------
            wo_sb = allocator.alloc_sbuf_tensor(
                shape=(_P_MAX, H), dtype=nl.bfloat16,
                block_dim=[n_ochunks], num_free_tiles=[n_ochunks])
            for oc in range(n_ochunks):
                nisa.dma_copy(dst=wo_sb[oc],
                              src=wot_hbm[col0 + oc * _P_MAX:col0 + (oc + 1) * _P_MAX, 0:H])
            # bias already HALVED on the host (pack_out_weights) so the two cores' partials sum to
            # exactly one bias. Broadcast across partitions (output columns are on the free axis).
            bo_bc = allocator.alloc_sbuf_tensor(shape=(_P_MAX, H), dtype=nl.float32, align_to=4)
            nisa.dma_copy(dst=bo_bc,
                          src=bo_hbm.ap(pattern=[[0, _P_MAX], [1, H]], offset=0))

        # ---- O STAYS IN SBUF, ALLOCATED EXACTLY ONCE. tp_out=True gives [D, 128 tokens] per Q
        # group (feature-major), which the projection contracts over, so O never transposes.
        # The list handed to the vendor is SLICES into o_feat, so _write_back_o_impl copies
        # straight into final position. ------------------------------------------------------
        n_grp = NP // _P_MAX
        o_feat = allocator.alloc_sbuf_tensor(
            shape=(_P_MAX, NP), dtype=nl.bfloat16,
            block_dim=[n_ochunks], num_free_tiles=[n_ochunks], align_to=32)
        o_sb = []
        for _lh in range(local_heads):
            oc = (_lh * D) // _P_MAX
            rr = (_lh * D) % _P_MAX
            # explicit loop, NOT a comprehension: the NKI tracer rejects comprehensions in a kernel.
            grp = []
            for _g in range(n_grp):
                grp.append(o_feat[oc][rr:rr + D, _g * _P_MAX:(_g + 1) * _P_MAX])
            o_sb.append(grp)

        attn_base = allocator.get_current_address()
        for lh in range(local_heads):
            if _big:
                # N>2048: load THIS head's K/V from HBM into the resident staging rings (only one
                # head live at a time -> K/V cost is one head's 24576 B/lane, not eight heads'
                # 196608). Then run the SAME single-section flatten the N<=2048 path uses: seqlen_k
                # =8192 < 10240 threshold => num_sections==1, no multi-section flash combine. The
                # inner's large-tile loop iterates 4x over _LARGE_TILE_SZ-sized buffers; O is written
                # straight into this head's o_feat SBUF slices (o_sb[lh]), exactly as N<=2048.
                for _i in range(n_ktiles):
                    nisa.dma_copy(dst=k_stage[_i], src=k_hbm[lh][_i])
                for _i in range(n_vtiles):
                    nisa.dma_copy(dst=v_stage[_i], src=v_hbm[lh][_i])
                # 1D python-list views [seq_tile] (the flatten indexes k_sb[k_tile_idx_in_section]
                # and v_sb[v_tile_idx] as flat lists -- same convention as k_tiles[lh]/v_tiles[lh]).
                k1d = []
                for _i in range(n_ktiles):
                    k1d.append(k_stage[_i])
                v1d = []
                for _i in range(n_vtiles):
                    v1d.append(v_stage[_i])
                _attention_cte(
                    q_priv[lh][0], None, None,
                    scale=scale, causal_mask=False,
                    tp_q=False, tp_k=False, tp_out=True,
                    cache_softmax=False,
                    k_cache_sbuf=k1d,
                    v_cache_sbuf=v1d,
                    out_o_hbm=o_sb[lh],
                    init_sbuf_addr=attn_base,
                    seqlen_k_valid=n_valid,
                )
            else:
                # N<=2048: the pinned single-section flatten (h8/h10 engine pins intact, bit-for-bit).
                for sub in range(PK):
                    if PK == 1:
                        k_l = k_tiles[lh]
                        v_l = v_tiles[lh]
                        o_l = o_sb[lh]
                    else:
                        # pack2: this image's K columns / V tiles / O slices of the shared buffers
                        k_l = [k_tiles[lh][0][:, sub * N:(sub + 1) * N]]
                        v_l = []
                        for _v in range(n_grp1):
                            v_l.append(v_tiles[lh][sub * n_grp1 + _v])
                        o_l = []
                        for _o in range(n_grp1):
                            o_l.append(o_sb[lh][sub * n_grp1 + _o])
                    _attention_cte(
                        q_priv[lh][sub], None, None,
                        scale=scale, causal_mask=False,
                        tp_q=False, tp_k=False, tp_out=True,
                        cache_softmax=False,
                        k_cache_sbuf=k_l,
                        v_cache_sbuf=v_l,
                        out_o_hbm=o_l,
                        init_sbuf_addr=attn_base,
                        seqlen_k_valid=n_valid,
                    )
                    allocator.set_current_address(attn_base)
            allocator.set_current_address(attn_base)

        # ---- PARTIAL out projection: tokens on partitions, output columns on the free axis
        # (chosen by the HBM write: result is token-major, so a row of consecutive output columns
        # is a 1024-byte contiguous run). 128 calls/layer/core (16 token blocks x 2 col blocks x
        # 4 chunks). --------------------------------------------------------------------------
        out_sb = allocator.alloc_sbuf_tensor(shape=(_P_MAX, _PSUM_FMAX),
                                             dtype=nl.bfloat16, align_to=32)
        n_ob = (H + _PSUM_FMAX - 1) // _PSUM_FMAX
        for tb in range(NP // _P_MAX):
            for ob in range(n_ob):
                ow = min(_PSUM_FMAX, H - ob * _PSUM_FMAX)   # ragged last column block (H%512!=0)
                ps_out = nl.ndarray((_P_MAX, _PSUM_FMAX), dtype=nl.float32, buffer=nl.psum,
                                    address=(0, 0))
                for oc in range(n_ochunks):
                    nisa.nc_matmul(
                        dst=ps_out[:, 0:ow],
                        stationary=o_feat[oc][:, tb * _P_MAX:(tb + 1) * _P_MAX],
                        moving=wo_sb[oc][:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
                        accumulate=(oc > 0),
                    )
                nisa.tensor_tensor(dst=out_sb[:, 0:ow], data1=ps_out[:, 0:ow],
                                   data2=bo_bc[:, ob * _PSUM_FMAX:ob * _PSUM_FMAX + ow],
                                   op=nl.add)
                nisa.dma_copy(
                    dst=result.ap(pattern=[[H, _P_MAX], [1, ow]],
                                  offset=out_slot * NT * H + rofs + tb * _P_MAX * H + ob * _PSUM_FMAX),
                    src=out_sb[:, 0:ow])
    return result


_ATTN = {g: wrap_nki_to_torch_hop(_attention_block_kernel[g]) for g in sorted({1, LNC})}


def _no_grad(*tensors) -> None:
    for t in tensors:
        if t is not None and t.requires_grad:
            raise RuntimeError(
                "attention_block registers no autograd rule and its packed weights are "
                "detached caches. Use under torch.no_grad()/eval().")


@nki_op("vit_enc::attention_block", mutates_args={})
def attention_block(
    h: torch.Tensor,
    wqk: torch.Tensor,
    bqk: torch.Tensor,
    tab: torch.Tensor,
    wvt: torch.Tensor,
    bv: torch.Tensor,
    wot: torch.Tensor,
    bo: torch.Tensor,
    scale: float,
    head_dim: int,
    n_valid: int,
    hb: int = 0,
    grid: int = LNC,
    nimg: int = 1,
    resident: int = 0,
    pack2: int = 0,
) -> torch.Tensor:
    """Fused norm1'd-hidden -> QKV+rope -> attention -> fused out-projection PARTIALS.

    Returns ``[LNC=2, N, H]`` bf16 per-core out-projection partials; K3's residual add sums the
    two cores. The two Scalar engine pins (V-scatter :276, QK bias-evict :458) are hardcoded.
    """
    _no_grad(h, wqk, bqk, tab, wvt, bv, wot, bo)
    # N>2048 (e.g. 64-frame clips) is NOT supported. The phase-1 memory fixes fit (~106 KiB/lane),
    # but the single-section vendor inner overflows phase 2 (measured 354912 B/lane vs the 196608
    # budget: exp_tp_sb/mm1_masked/exp_sb scale with num_large_tiles_per_section=ceil(N/2048)=4).
    # The shrinkable phase-2 region (78848 B/lane) is smaller than the 158304 overflow, so
    # streaming O / shrinking o_feat cannot close the gap; re-enabling flash sectioning fits but
    # is numerically wrong (rel_l2 0.90). Refuse here rather than emit a non-fitting or wrong
    # kernel. See the V-JEPA2 kernels' verification notes. NKI kernels cannot `raise`, so the guard lives here.
    N = h.shape[0] // nimg
    if N > 2048:
        raise NotImplementedError(
            f"attention_block N={N} (>2048, e.g. 64-frame clips) is not supported: the "
            f"single-section vendor inner does not fit SBUF (phase-2 peak measured 354912 B/lane "
            f"vs the 196608 budget). Streaming O or shrinking o_feat cannot close the 158304 "
            f"B/lane gap, and flash sectioning is numerically wrong (rel_l2 0.90). See "
            f"the V-JEPA2 kernels' verification notes. Use N<=2048 (up to 16-frame clips) -- the bit-for-bit "
            f"shipping path."
        )
    if bqk.dim() == 3 and bqk.shape[1] != 128:
        bqk = pack_qk_bias(bqk)
    return _ATTN[grid](h, wqk, bqk, tab, wvt, bv, wot, bo, scale, head_dim, n_valid, hb, grid, nimg, resident, pack2)