File size: 66,864 Bytes
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddaad99
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddaad99
 
 
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55474c5
 
 
 
ddaad99
 
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
ddaad99
 
 
 
 
 
 
55474c5
 
 
ddaad99
 
 
 
55474c5
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddaad99
 
55474c5
ddaad99
 
 
55474c5
 
 
 
 
ddaad99
 
 
 
 
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
ddaad99
55474c5
ddaad99
55474c5
 
ddaad99
 
 
 
 
55474c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55474c5
 
 
 
 
 
 
 
 
 
ddaad99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
# SPDX-License-Identifier: Apache-2.0
"""Python API: BEVFormer-small (Autoware tensorrt_bevformer) on one Tenstorrent Blackhole p150.

    from tt_bevformer import BEVFormer

    with BEVFormer.from_pretrained(device_id=0) as model:  # weights -> HF cache, device open, traces captured
        out = model(**load_sample("code/tt_bevformer/samples/synthetic_6cam.json"))  # see PYTHON.md for every input form
    print(out.to_dict())  # the same JSON as POST /predict

The contract shared by every bundle of the Autoware collection is the vendored ``ttaw.api_base.ModelBase``
(BUNDLE_CONVENTIONS.md section 8): ``from_pretrained`` resolves the pinned weights before it claims the chip, opens it
(ETH dispatch, 12x10), builds the graph and captures every trace variant in ``warmup_variants``, so the first call is
as fast as the later ones; calls are serialised by a lock (one chip, batch 1) and fill ``timing_ms``; ``close()`` is
idempotent, also runs at interpreter exit, and closes the chip only if the model opened it. The HTTP server
(``tt_bevformer.server.app``) calls this class, so ``/predict`` and ``model(...)`` agree bit for bit.

This module holds the identity of the port and its hooks, which delegate to ``tt_bevformer.reference`` (host pre- and
post-processing ported from Autoware) and ``tt_bevformer.tt`` (the ttnn graph, run by a ``ttaw.trace.TraceRunner``).
Importing it has no side effects: import ttnn / torch inside the hooks, never at module level.
"""
from __future__ import annotations

from typing import Any, Dict, Mapping, Optional

import numpy as np

from . import io as tio
from .device import DEVICE_DEFAULTS
from .ttaw.api_base import ModelBase
from .ttaw.knobs import Knob, Knobs
from .ttaw.outputs import Detections3D

__all__ = ["BEVFormer", "Output", "KNOBS"]

# The result class of this model (ttaw.outputs: Detections3D, Detections2D, Segmentation3D, Mask2D, Trajectory).
Output = Detections3D

# Load-time switches (read once at build; env BEVFORMER_<NAME>). Each optimization adds its knob here (A/B switch).
KNOBS = Knobs("BEVFORMER", [
    Knob("PRECISION", "HiFi2+fp32+l1acc:w=bf16", "precision of every conv / linear (ttaw.precision spec; PLAN D20 "
                                                 "reduced-precision default): HiFi2, fp32 accumulation, packer L1 "
                                                 "accumulation (fp32 K-block partials), bf16 weights and biases; bf16 "
                                                 "activations. The full-precision path (the frozen gates): "
                                                 "HiFi4+fp32+l1acc:w=fp32 (serve profile 'accurate')"),
    Knob("PRECISION_RULES", "*.conv_offset=HiFi2+fp32+l1acc:w=bf16;img_backbone.layer3.*=HiFi2+fp32+l1acc:w=bfp8",
         "per-module precision overrides (PLAN D20 reduced-precision path): "
                                "'glob=spec;glob=spec' on the module names (img_backbone.layer3.0.conv2, "
                                "img_backbone.layer3.0.conv2.conv_offset, encoder.layers.0.ffn.0, ...), first match "
                                "wins, ttaw.precision specs; modules no rule matches keep PRECISION. A fused "
                                "mmres_k block needs one weight dtype for its conv3 / downsample / next conv1. Default: bfp8 weights for "
         "layer 3 (its DCN offset convs bf16); 'none' = PRECISION everywhere"),
    Knob("DEC_ATTN", "sdpa", "decoder self-attention core: matmul (fp32 scores QK^T + mask, fp32 softmax, bf16 "
                               "probabilities @ V, concat heads) or sdpa (ttnn's flash SDPA with a bf16 pad-key mask, "
                               "heads concatenated by the op; PLAN D20 reduced-precision path)",
         choices=("matmul", "sdpa")),
    Knob("MSDA", "kernel", "multi-scale deformable attention: kernel (msda_k, OPT_PLAN #1: Q16.16 positions, fp32 "
                         "corner and softmax weights, fp32 sums), grid (the fp32-grid decomposition: grid_sample + "
                         "exact 0/1 matmuls) or fused (the stock bf16 op); per call site 'kernel,dec=grid' etc. "
                         "(sites tsa, sca, dec; tt.msda.site_modes)"),
    Knob("MSDA_PX", True, "msda_k sites in pixel units: sampling weights x 2^16 and pixel bases, so the int32 "
                           "positions are one add + typecast (no broadcast scale / offset passes over the per-head "
                           "grids)"),
    Knob("MSDA_VALUE", "tile", "msda_k value source of the TSA / decoder sites: rm (the head maps untilized into L1) or "
                              "tile (the TILE head split in L1, read as face rows: no untilize)", choices=("rm", "tile")),
    Knob("MSDA_LOC", "compact", "msda_k sampling positions of the TSA / SCA sites (with MSDA_PX): heads (the sampling "
                              "projection in 32-column head blocks, split per head, base / shift adds and the int32 "
                              "cast on the split) or compact (only the 2P real columns per head, msda_k reads the "
                              "merged map: no head split, 4x / 2x fewer columns; the same values)",
         choices=("heads", "compact")),
    Knob("MSDA_SOFTMAX", "kernel", "softmax of the TSA / SCA attention logits (with MSDA_LOC=compact): op (head-block "
                               "projection, head split, ttnn.softmax) or kernel (compact projection, msda_k rebuilds "
                               "each head's logit tile and runs ttnn.softmax's LLK sequence itself: same weights)",
         choices=("op", "kernel")),
    Knob("SCA_GATHER", "tile", "SCA rebatching gathers (query rows per camera, the scatter-add's 3 gathers): rm (untilize + "
                             "zero-row pad + ttnn.embedding + tilize) or tile (rowgather_k: rows copied straight "
                             "between TILE maps; the same bytes)", choices=("rm", "tile")),
    Knob("BEV_MM", "fast", "BEV-side linears: default (ttnn.linear's own configs) or tuned (tt.linconv.TUNED_BEV: the "
                              "same K blocking with faster M / N blocking or a 2D grid; bit-identical) or fast (PLAN D20: 1D configs with K "
                              "blocks of up to 8 tiles for the >= 4096-row linears; not bit-identical)",
         choices=("default", "tuned", "fast")),
    Knob("MSDA_PAIRS", "all", "value maps as pixel-pair maps (tt.pairmap_k: 128-byte pages of two neighbouring sticks): "
                              "msda_k reads the two upper / lower corners of a point with one request and tilizes them "
                              "(4x fewer reads; bit-identical): off, sca (the SCA value state) or all (SCA + TSA)",
         choices=("off", "sca", "all")),
    Knob("PAIRMAP", "v2", "pairmap_k (the TSA / SCA value pair maps): v1 (4 face-row reads of 32 bytes + one write per "
                          "128-byte page) or v2 (per tile of 32 pixels x one head: one tile read, a pack_untilize, one "
                          "128-byte write per page: ~1.1 NoC requests per page instead of 5; the same bytes)",
         choices=("v1", "v2")),
    Knob("MSDA_SKIP", False, "msda_k: sampling points wholly outside the map (and the SCA's padding rows) issue no "
                             "reads (weight 0), items strided over the cores"),
    Knob("DCN", "kernel", "DCNv2 sampling of the 26 ResNet conv2: kernel (dcn_k, OPT_PLAN #3: Q16.16 tap positions, "
                       "fp32 bilinear x mask weights, the modulated im2col in one program) or grid (grid_sample + "
                       "mask expand matmul + multiply)", choices=("grid", "kernel")),
    Knob("DCN_IM2COL", "l1", "DCN modulated im2col (kernel mode + tuned GEMM): dram (one [NHW, 9C] tensor in DRAM, "
                               "102 MB per layer-3 call) or l1 (row chunks of <= ~300 KB per core written by dcn_k into "
                               "an L1 height-sharded tensor the GEMM reads in place; same K blocking: bit-identical; "
                               "layer 3 only) or l1_all (layer 4 as well: D20, bf16 weights)",
         choices=("dram", "l1", "l1_all")),
    Knob("DCN_SCALE", "full", "dcn_k position scale: row (a [1, 32] row broadcast multiply) or full (the scale repeated "
                             "over the rows: no broadcast, the same fp32 products)", choices=("row", "full")),
    Knob("DCN_OFFSET_L1", True, "DCN offset convs on ttnn's L1 conv path (no DRAM-slicing unflatten / flatten "
                                 "reshape copies of the conv1 rows)"),
    Knob("BACKBONE_MM", "exact2", "backbone matmul configs (tt.linconv.TUNED, measured K blocks of 4-8 tiles): conv (stock: "
                                "ttnn.conv2d's matmul for the 1x1 convs, ttnn.linear's default for the DCN GEMM), dcn "
                                "(the tuned config for the DCN im2col GEMM only), exact (dcn + the 1x1 / stride-1 convs "
                                "whose ttnn.linear config is bit-identical to conv2d's: tt.linconv.EXACT) or linear "
                                "(dcn + every 1x1 / stride-1 conv as tuned ttnn.linear); exact2: exact plus 2D-grid configs with "
                                "the same K blocking (tt.linconv.EXACT2 / TUNED2; bit-identical)",
         choices=("conv", "dcn", "exact", "exact2", "linear")),
    Knob("BACKBONE_S2", "prod", "the 1x1 / stride-2 convs of layer3.0 / layer4.0 (conv1, downsample): conv (ttnn.conv2d on "
                                "the DRAM-slicing path) or sub (one exact even-pixel row gather shared by both, then "
                                "ttnn.linear; not bit-identical: other K blocking) or prod (PLAN D20 round 3: also "
                                "layer2.0's, and the last block of layers 1-3 writes only the even pixels' sticks "
                                "from its fused mmres_k program: no full-resolution stage output, no untilize / row "
                                "gather / conv2d DRAM slicing; layer 2 not bit-identical, layers 3-4 as sub)",
         choices=("conv", "sub", "prod")),
    Knob("MSDA_FRAC16", True, "msda_mk's readers write each tap fraction q as the fp32 bits of 1 + q / 2^16 (a shift "
                               "and an or instead of the leading-zero fp32 conversion per point) and the compute "
                               "kernel's SFPU subtracts 1 (exact: bit-identical)"),
    Knob("SCA_COPY", "kernel", "the SCA value state's DRAM -> L1 copy before each SCA call: op (ttnn.to_memory_config) or "
                           "kernel (repage_k: batched page reads / writes on both data-movement RISC-Vs; bit-identical)",
         choices=("op", "kernel")),
    Knob("DEC_VALUE", "fused", "the decoder's cross-attention values: split (value projection to DRAM, then "
                               "nlp_create_qkv_heads into L1) or fused (the projection straight to L1, read by msda_k "
                               "head-interleaved: no head split; bit-identical)", choices=("split", "fused")),
    Knob("S2_SKIP", True, "with BACKBONE_S2=prod: the producing mmres_k computes / reads / writes only the tile rows "
                           "with a pixel on an even image row (the others feed no even pixel; the same values: "
                           "bit-identical)"),
    Knob("BACKBONE_C3", "l1_2", "the layer-1 3x3 convs: slice (ttnn.conv2d's DRAM-slicing path) or l1 (the L1 path with "
                                 "8-tile activation blocks; bit-identical) or l1_2 (also the layer-2 3x3 convs on the "
                                 "L1 path, their outputs read in place by mmres_k)", choices=("slice", "l1", "l1_2")),
    Knob("RES_L1", True, "layer 3's conv3 (1x1, 256 -> 1024) writes its output in L1 shards (conv2d's K blocking) and "
                          "the residual add + ReLU reads it in place, writing the block output to DRAM (bit-identical)"),
    Knob("BACKBONE_RES", "next", "conv3 + residual add + ReLU of the layer-1 / layer-2 bottlenecks: stock (ttnn.linear "
                                 "with the EXACT2 config, then ttnn.add: the 180 / 90 MB maps go to DRAM and back) or "
                                 "fused (tt.mmres_k: one kernel replays the linear's and the add's compute steps, the "
                                 "layer1.0 downsample in the same kernel; bit-identical) or next (fused, and the same "
                                 "kernel also computes the next block's conv1 from the rows it wrote; bit-identical)",
         choices=("stock", "fused", "next")),
    Knob("DEC_ATTN_L1", True, "decoder self-attention: the [8, 928, 928] scores, masked scores and probabilities in L1 "
                                "instead of DRAM (the same ops and configs; bit-identical)"),
    Knob("KERNEL_TILIZE", "std", "tilize inside the custom kernels dcn_k / msda_k: std (tilize_block) or fast (the "
                                 "Blackhole fast tilize; a lossless bf16 reorder: bit-identical)", choices=("std", "fast")),
    Knob("DCN_GLUE", "kernel", "DCN offset glue (scale multiply, base add, int32 cast, sigmoid on the [NHW, 32] offset "
                             "rows): dram or l1 (the intermediates in L1; the same ops: bit-identical) or kernel "
                             "(dcn_k runs glue_k's steps per block on the offset conv's own L1 shards: no glue "
                             "program, no sharded-to-interleaved copy; bit-identical)",
         choices=("dram", "l1", "kernel")),
    Knob("DCN_SUM", "mul", "dcn_k's weighted sum of the 4 bilinear corners (with DCN_BATCH): mul (fp32 column-broadcast "
                           "multiplies with packer-L1 accumulation, then a bf16 copy pass) or mm (PLAN D20: the corner "
                           "weights as diagonal fp32 tiles, out = sum_c diag(w_c) @ X_c as one DEST accumulation of "
                           "matmuls, one bf16 pack)", choices=("mul", "mm")),
    Knob("DCN_SAMPLE", "bilinear", "dcn_k's sampling of each DCN tap: bilinear (4 corners; the reference semantics) or "
                                  "nearest (PLAN D20 round 3: the nearest pixel, round half up: one gathered stick "
                                  "per tap, the items alternating between the two gather RISC-Vs, mask x tile, no "
                                  "corner weights / sum; not bit-identical, judged by the task gates) or nearest_l4 "
                                  "(layer 4's DCNs only)",
         choices=("bilinear", "nearest", "nearest_l4")),
    Knob("DCN_PAIRS", False, "dcn_k reads a horizontal pixel-pair map of its input (dcnpair_k: page (n, y, i) = pixels "
                             "i - 1 and i): one 2C-wide request per sampling row and corner pair instead of two "
                             "(half the gather requests; the same values: bit-identical)"),
    Knob("DCN_OUT", "l1", "with RES3_PARTS: where the layer-3 DCN GEMM writes its row chunks for the fused block's "
                             "mmres_k: dram or l1 (interleaved L1: no 11 MB DRAM round trip per block; the same "
                             "values)", choices=("dram", "l1")),
    Knob("DCN_BATCH", True, "dcn_k: the 4 corners of an item tilized back to back, then the 4 weighted multiplies "
                              "(one tilize and one multiply init per item instead of four; the same products and "
                              "accumulation order: bit-identical)"),
    Knob("BACKBONE_C3_OUT", "shard", "with BACKBONE_RES fused / next: the layer-1 3x3 conv outputs stay height-sharded "
                                    "in L1 (shard) and mmres_k reads them in place, or go to DRAM (dram)",
         choices=("dram", "shard")),
    Knob("STEM", "plain", "stem input: plain (NHWC 3-channel rows, the 7x7 / stride-2 conv) or s2d (space-to-depth "
                          "16-channel rows uploaded as 368 x 640 maps, the equivalent 4x4 / stride-1 conv: 4x fewer "
                          "and 32-byte upload pages / DRAM slices)", choices=("plain", "s2d")),
    Knob("UPLOAD", "rows", "image upload layout: pixels (one 6-byte page per pixel: H2D 141 ms) or rows (one 7,680-byte "
                             "page per image row: H2D 4 ms, then one exact 7.8 ms ROW_MAJOR reshape on the device inside "
                             "the image trace)", choices=("pixels", "rows")),
    Knob("STEM_IN", "kpre", "stem input with UPLOAD=rows and STEM=plain: slice (the image rows reshaped to 6-byte pixel "
                             "pages, the stem conv on ttnn's DRAM-slicing path) or kin (per camera a K-IN kernel copies "
                             "the rows into the conv's L1 input shards, the conv on the L1 path, the pool per camera; "
                             "bit-identical; tt.kin) or kpre (the uint8 node images go up, one input per camera, "
                             "and tt.kpre runs the C14 pre-processing on the device straight into those shards: "
                             "bit-identical to HOST_PRE + kin; the host keeps only the node resize of other sizes)",
         choices=("slice", "kin", "kpre")),
    Knob("OUTPUTS", "tile", "the bev trace's outputs: pack (pack_outputs: one ROW_MAJOR row, one D2H copy) or tile (the "
                            "two fp32 TILE maps read as they are: no device reshape)", choices=("pack", "tile")),
    Knob("MAX_STREAMS", 4, "BEV-history streams kept on the device (PLAN D16: one prev_bev bank per stream id; a new "
                           "id beyond it evicts the least recently used one)"),
    Knob("MAX_GAP_S", 2.0, "a stream restarts its history (use_prev_bev = 0) when its timestamp jumps by more than "
                           "this many seconds or goes backwards (PLAN D13: reset on a large time gap)"),
    Knob("SHIFT_MODE", "surrogate", "TSA reference shift: DerryHub's atan2 surrogate (the deployed graph) or atan2 "
                                    "(training); they differ only for dx == 0 or dx < 0 == dy (SPEC 4.3)",
         choices=("surrogate", "atan2")),
    Knob("CAN_BUS_LAYOUT", "training", "can_bus[7:16] slot order of the ego dynamics: BEVFormer training (accel, "
                                       "rotation rate, velocity) or the Autoware node's literal KinematicState "
                                       "order (PORT_LOG.md E3)", choices=("training", "autoware")),
    Knob("HOST_PRE", "bf16", "host pre-processing (node resize + C14 preset): bf16 (host.fast.preprocess_rows_bf16: "
                             "the node resize, the preset and the bf16 cast of the upload fused in one numba pass per "
                             "camera, bit-exact), numba (host.fast: bit-exact twins, NHWC float32 rows written "
                             "directly, RGB read as decoded) or numpy (host.preprocess); the numba paths fall back to "
                             "numpy when numba is not installed (bf16 also to numba with STEM=s2d)",
         choices=("numpy", "numba", "bf16")),
    Knob("HOST_MALLOC", "keep", "host heap: default (glibc's) or keep (mallopt: large per-frame buffers reuse the "
                                   "retained heap instead of fresh mmap pages; host.fast.keep_heap)",
         choices=("default", "keep")),
    Knob("RES3_NG", 4, "layer-3 conv3 + residual add + ReLU (with RES_L1): 0 = L1-sharded linear + ttnn.add (stock), "
                       "2 / 4 = one mmres_k program with the fp32 conv3 weight split by columns over that many core "
                       "groups (bit-identical)", choices=(0, 2, 4, 8)),
    Knob("ACT8", True, "with BACKBONE_RES fused / next (PLAN D20 path): the layer-1 / layer-2 residual stream between "
                        "the fused blocks of a stage in bfp8 (mmres_k writes and reads it; the stage outputs stay "
                        "bf16): about half the DRAM traffic of these DRAM-bound programs"),
    Knob("ACT8_L3", True, "with RES3_NEXT (PLAN D20 path): the layer-3 residual stream between the fused blocks in bfp8 "
                           "(each block's output is read only by the next block's mmres_k; the last block's output "
                           "stays bf16)"),
    Knob("RES3_NEXT", True, "with RES3_NG > 0 (PLAN D20 path): the layer-3 mmres_k program also computes the next "
                             "block's conv1 (1024 -> 256, ReLU) from the rows it writes (one core group, whole rows; "
                             "the conv3 and conv1 weights resident in L1 together: needs bfp8 layer-3 weights, "
                             "PRECISION_RULES)"),
    Knob("RES3_RM", "on", "with RES3_NEXT (PLAN D20 path): the fused layer-3 program writes the next block's conv1 "
                           "output as the row-major L1 map that the DCN's offset conv and dcn_k read (untilized in "
                           "the kernel: no untilize program, no DRAM round trip of the [22080, 256] map; the same "
                           "values): off, on (L1 interleaved) or shard (height shards of the offset conv's layout: "
                           "no interleaved-to-sharded copy either: dcn_k's sharded reads are slow) or dual (the interleaved "
                           "map for dcn_k plus a height-sharded twin for the offset conv)",
         choices=("off", "on", "shard", "dual")),
    Knob("RES3_L1", False, "with RES3_NEXT and ACT8_L3 (PLAN D20 path): the layer-3 bfp8 residual stream between the "
                            "fused blocks (24 MB, read only by the next block's mmres_k) in L1 interleaved instead of "
                            "DRAM (the same values)"),
    Knob("L4_L1", False, "(PLAN D20 path) the layer-4 1x1 convs (conv1, conv3, the layer4.0 strided pair) write L1 "
                          "interleaved, so the residual adds keep the block outputs in L1 ([5536, 2048] maps: no DRAM "
                          "round trips; the same values)"),
    Knob("RES3_PARTS", True, "with RES3_NG > 0: the fused layer-3 block reads the DCN GEMM's row chunks in place "
                              "(no [22080, 256] chunk concat)"),
    Knob("UPLOAD_PIPE", True, "with HOST_PRE=bf16, UPLOAD=rows and STEM_IN=kin: one image-rows input per camera, "
                               "pre-processed in a worker thread while the calling thread uploads the cameras "
                               "already done (the H2D of camera i overlaps the pre-processing of camera i + 1; "
                               "api.preprocess then excludes the image pass, api.device includes it)"),
    Knob("BEV_USE", "variants", "TSA queue blend: blend (one bev trace: use * [rot(prev_bev); bev_queries] + (1 - "
                                "use) * [query; query], 3 binary ops per blend, 5 blends per frame) or variants (a "
                                "second bev trace, bev_prev, replayed when use_prev_bev == 1: the blends are the "
                                "identities, no ops; the bev trace for fresh / reset frames; bit-identical)",
         choices=("blend", "variants")),
    Knob("TSA_VIN_SHARED", True, "with BEV_USE=variants: the TSA value input [rot(prev_bev); bev_queries] (the same "
                                  "in every layer of bev_prev) is concatenated once per frame instead of once per "
                                  "layer (bit-identical)"),
    Knob("DEC_QK", "grid", "decoder self-attention QK^T [8, 928, 32] x [8, 32, 928]: default (ttnn's 2D config, 4 x 4 "
                           "cores) or grid (3 x 3 tiles per core on a 10 x 10 grid; K is one tile: bit-identical)",
         choices=("default", "grid")),
    Knob("GLUE_K", True, "the fp32 glue in front of msda_k (int32(cols + base [+ shift]): 2-3 ops per TSA / SCA call) "
                          "and dcn_k (int32(off * S + B), sigmoid(off): 4 ops per DCN) as one tt.glue_k program each, "
                          "with the stock LLK calls in the same order (bit-identical)"),
    Knob("TSA_VP_L1", True, "with MSDA_PAIRS=all: the TSA value projection writes L1 directly for pairmap_k (no DRAM "
                             "-> L1 copy of the [2, 22528, 256] projection; the same ttnn.linear program: "
                             "bit-identical)"),
    Knob("MSDA_MK", "all", "the D19 MSDA block module megakernel (tt.msda_mk): one program per encoder MSDA call runs the "
                           "query concat, the compact sampling + attention projections (fp32 weights resident in L1, "
                           "in0_block_w 1, packer L1 accumulation: ttnn.linear's steps), the loc glue (glue_k's steps) "
                           "and the msda_k gather / softmax / sum, instead of 5 (TSA) / 4 (SCA) programs; needs "
                           "MSDA=kernel, MSDA_PX, MSDA_LOC=compact, MSDA_SOFTMAX=kernel, MSDA_PAIRS: off, sca, tsa or "
                           "all (bit-identical; fp32 weights and HiFi4 whatever PRECISION says)", choices=("off", "sca", "tsa", "all")),
    Knob("SCA_ACTIVE", True, "with MSDA_MK sca / all: the SCA megakernel computes only the active 32-row blocks of "
                               "each camera's rebatched rows (a per-calibration runtime page, sca_nblk: the work "
                               "split follows the rig's coverage; the padding rows behind each camera's rows are "
                               "never gathered, so the outputs are bit-identical)"),
    Knob("SCA_BASE", "kernel", "with MSDA_MK sca / all: frame (the SCA's pixel-unit compact base map base * S + O is "
                              "derived from sca_base in every bev replay: multiply, add, untilize, slice, repeat, "
                              "tilize) or kernel (msda_mk reads the raw sca_base, its reader duplicates the point "
                              "columns into both column halves of a tile and the front applies S and O with the "
                              "same SFPU mul / add before the cols + base add: bit-identical)",
         choices=("frame", "kernel")),
    Knob("SCA_SCATTER", "kernel", "with SCA_GATHER=tile: the SCA tail (the 3 row gathers of the camera outputs, the 2 "
                                 "adds and the 1 / cameras multiply) as stock programs or as one scatter_k program "
                                 "(kernel: the stock ops' FPU add / SFPU multiply steps; bit-identical)",
         choices=("stock", "kernel")),
    Knob("MMRES_MCAST", True, "mmres_k (the fused conv3 + residual [+ next conv1] programs of layers 1-3): the resident "
                              "weights are read from DRAM once per grid row by the row's first core and multicast "
                              "along the row (on) instead of every core reading all of them (off; the layer-3 "
                              "program read 120 x 556 KB of weights per call); the same bytes: bit-identical"),
    Knob("NECK_L1", True, "the FPN 3x3 conv on ttnn's L1 path (its [5536, 256] input from DRAM: no DRAM-slicing "
                            "unflatten / flatten reshape copies)"),
    Knob("ENC_L1", "all", "encoder intermediates in L1 interleaved instead of DRAM (the same ttnn.linear / msda_mk "
                          "programs: the same values): off, on (the FFN hidden map [22528, 512] and the TSA "
                          "megakernel output) or all (also the TSA / SCA output projections and the FFN output; "
                          "max = also the FFN input LayerNorm and the query + position sum was measured: L1 clash)",
         choices=("off", "on", "all")),
    Knob("MSDA_MK_MCAST", True, "with MSDA_MK: the megakernel's resident projection weights (fp32) are read from DRAM "
                                "once per grid row by the row's first core and multicast along the row (on) instead "
                                "of every core reading all of them (off); the same bytes: bit-identical"),
    Knob("HOST_RESIZE", "fast", "with STEM_IN=kpre: the node's uint8 INTER_LINEAR resize of non-node-size cameras "
                                "(e.g. 1920x1080): fast (host.fast._node_images_fast: all cameras in one parallel "
                                "numba pass, the column phases unrolled with constant weights, 32-bit arithmetic; "
                                "node-size images copied into the staging buffers in parallel) or rows (one row-cached "
                                "pass per camera); bit-identical", choices=("fast", "rows")),
    Knob("HOST_THREADS", 4, "threads of the host image pre-processing (C14 preset, one camera per thread)"),
    Knob("VALUES_DIRECT", True, "with MSDA_PAIRS: the SCA value projection writes L1 directly and pairmap_k writes "
                                 "the sca_value states itself (no DRAM -> L1 copy of the projection, no copy of the "
                                 "maps into the states; bit-identical)"),
    Knob("SCA_VALUE", "l1", "with MSDA_PAIRS: msda_k's SCA value source: l1 (the DRAM state copied into L1 per layer) "
                            "or dram (read in place from the state; bit-identical)", choices=("l1", "dram")),
    Knob("HOST_STAGING", True, "with STEM_IN=kpre: the six node images are written straight into persistent ttnn "
                                "host buffers (a pool of up to 4 sets; ttaw HostStaging) by the node resize (or one copy "
                                "for node-size images) instead of a ttnn.from_torch copy per frame"),
    Knob("PIPELINE", 1, "concurrent calls (OPT_PLAN R5-3): 0 = one call at a time under the model lock; 1 = per-stage "
                        "ordering: the request parsing, node resize and image host tensors of frame k + 1 run while "
                        "frame k is on the device, the post-processing runs outside every lock (frames take the "
                        "device in the order they finished pre-processing; outputs equal serial calls); 2 = also "
                        "the 26 MB camera upload of frame k + 1 on CQ1 while frame k's bev trace runs (needs a "
                        "device opened with 2 command queues, BEVFORMER_NUM_CQS=2; otherwise 1)",
         choices=(0, 1, 2)),
])



def tt_model_kwargs(k: Any, *, banks: int = 0, stem_in: Optional[str] = None, img_cams: bool = False) -> Dict[str, Any]:
    """The ``tt.model.TtBEVFormer`` keyword arguments for the knob values ``k`` (``KNOBS.read()``): the one mapping
    shared by the API and the device PCC gates (``tests/test_pcc_device.py``), so the gates measure the shipped
    device path. ``stem_in`` overrides the STEM_IN mapping (the API resolves kpre against numba; the gates feed the
    golden network input through the K-IN rows path); ``img_cams`` is the API's UPLOAD_PIPE decision."""
    if stem_in is None:
        stem_in = "kin" if str(k.STEM_IN) == "kpre" else str(k.STEM_IN)
    return dict(precision=str(k.PRECISION), banks=int(banks), msda=str(k.MSDA), dcn=str(k.DCN),
                dcn_offset_l1=bool(k.DCN_OFFSET_L1), msda_px=bool(k.MSDA_PX), msda_tile=k.MSDA_VALUE == "tile",
                backbone_mm=str(k.BACKBONE_MM), msda_skip=bool(k.MSDA_SKIP), stem=str(k.STEM),
                upload=str(k.UPLOAD), stem_in=stem_in, msda_loc=str(k.MSDA_LOC),
                backbone_s2=str(k.BACKBONE_S2), msda_softmax=str(k.MSDA_SOFTMAX),
                dcn_im2col=str(k.DCN_IM2COL), sca_gather=str(k.SCA_GATHER), bev_mm=str(k.BEV_MM),
                dcn_scale=str(k.DCN_SCALE), backbone_c3=str(k.BACKBONE_C3), outputs=str(k.OUTPUTS),
                res_l1=bool(k.RES_L1), msda_pairs=str(k.MSDA_PAIRS), backbone_res=str(k.BACKBONE_RES),
                dec_attn_l1=bool(k.DEC_ATTN_L1), kernel_tilize=str(k.KERNEL_TILIZE), dcn_glue=str(k.DCN_GLUE),
                dcn_batch=bool(k.DCN_BATCH), c3_out=str(k.BACKBONE_C3_OUT), img_cams=bool(img_cams),
                res3_ng=int(k.RES3_NG), res3_parts=bool(k.RES3_PARTS), bev_prev=str(k.BEV_USE) == "variants",
                vin_shared=bool(k.TSA_VIN_SHARED), dec_qk=str(k.DEC_QK), glue_k=bool(k.GLUE_K),
                tsa_vp_l1=bool(k.TSA_VP_L1), values_direct=bool(k.VALUES_DIRECT), sca_value=str(k.SCA_VALUE),
                msda_mk=str(k.MSDA_MK), sca_active=bool(k.SCA_ACTIVE),
                sca_base=str(k.SCA_BASE), sca_scatter=str(k.SCA_SCATTER),
                precision_rules=str(k.PRECISION_RULES), dec_attn=str(k.DEC_ATTN), res3_next=bool(k.RES3_NEXT), act8=bool(k.ACT8),
                act8_l3=bool(k.ACT8_L3), mmres_mcast=bool(k.MMRES_MCAST),
                msda_mk_mcast=bool(k.MSDA_MK_MCAST), res3_rm={"off": False, "on": True}.get(str(k.RES3_RM), str(k.RES3_RM)),
                dcn_sum=str(k.DCN_SUM), dcn_pairs=bool(k.DCN_PAIRS), dcn_sample=str(k.DCN_SAMPLE), s2_skip=bool(k.S2_SKIP), msda_frac16=bool(k.MSDA_FRAC16), sca_copy=str(k.SCA_COPY), dec_value=str(k.DEC_VALUE),
                dcn_out=str(k.DCN_OUT), pairmap=str(k.PAIRMAP),
                res3_l1=bool(k.RES3_L1), l4_l1=bool(k.L4_L1),
                enc_l1=str(k.ENC_L1), neck_l1=bool(k.NECK_L1))


class BEVFormer(ModelBase):
    """BEVFormer-small (Autoware tensorrt_bevformer) on one Blackhole p150. Create it with :meth:`from_pretrained`."""

    MODEL_NAME = "bevformer-p150"
    ENV_PREFIX = "BEVFORMER"  # prefix of the environment knobs (SERVING.md section 3.4)
    # No AutowareFoundation repo exists for BEVFormer (Autoware's bevformer_small.onnx is not obtainable, SPEC
    # section 1): the user-decided mirror of the official bevformer_small_epoch_24.pth state_dict (PLAN.md D13).
    DEFAULT_REPO = "changh95/bevformer-small-weights"
    DEFAULT_TAG = None  # the mirror has no tags; DEFAULT_REVISION is the commit `main` held when it was validated
    DEFAULT_REVISION = "0e6d9d9c7b8aca95820b264491e19a72d10f0608"
    WEIGHTS_LICENSE = ("Apache-2.0 (changh95/bevformer-small-weights mirror card; BEVFormer authors' checkpoint, "
                       "trained on nuScenes, CC BY-NC-SA 4.0)")
    ALLOW_PATTERNS = ["bevformer_small_epoch_24.safetensors", "provenance.json"]
    VARIANTS = ["default"]  # load-time: selects weights files and trace shapes
    DEFAULT_VARIANT = "default"
    INPUT_KIND = "multicam"  # lidar | camera | multicam | lidar+multicam | planner
    # The network's camera order: nuScenes info order, baked into the learned cams_embeds[6] (SPEC section 2.1)
    CAMERA_ORDER = ("CAM_FRONT", "CAM_FRONT_RIGHT", "CAM_FRONT_LEFT", "CAM_BACK", "CAM_BACK_LEFT", "CAM_BACK_RIGHT")
    POINT_FIELDS = tio.DEFAULT_POINT_FIELDS  # columns the network consumes, in order
    # The Autoware label the node publishes per network class (AW/src/ros_utils.cpp:70-94: bus, trailer and
    # construction_vehicle -> TRUCK; barrier, traffic_cone -> UNKNOWN); the network class names are
    # reference.config.CLASS_NAMES (meta.class_names)
    LABELS = ["CAR", "TRUCK", "TRUCK", "TRUCK", "TRUCK", "UNKNOWN", "MOTORCYCLE", "BICYCLE", "PEDESTRIAN", "UNKNOWN"]
    # Per-request knobs: name -> (type, min, max, default). Only host-side post-processing belongs here; anything
    # that changes a device shape or the weights is a load-time option (BUNDLE_CONVENTIONS.md section 9).
    RUNTIME_PARAMS = {
        "score_threshold": (float, 0.0, 1.0, 0.2),   # post_process_params.score_thre (AW/config/bevformer.param.yaml)
        "max_num": (int, 1, 9000, 300),              # the node's top-k over 900 x 10 scores (get_parameter_or 300)
        "physical_boxes": (bool, None, None, False),  # False: the node's message (size w,l,h; yaw -rot+pi); True:
    }                                                 # size l,w,h and the length axis' heading -rot-pi/2 (SPEC 5.1)
    # Model-specific input keyword arguments handed to _prepare (e.g. PointPainting ("rois",)); planner models set
    # INPUT_SCHEMA = {name: (shape, dtype)} of their ONNX inputs (ttaw API.md section 10).
    # ``ego``: the frame's CAN dynamics {"accel", "rotation_rate", "velocity"} (base_link, training layout of
    # can_bus[7:16]; host.temporal); the ego pose itself comes in stream.T_world_from_ego
    EXTRA_INPUTS = ("ego",)
    DEVICE_DEFAULTS = DEVICE_DEFAULTS  # validated open parameters (device.py)

    # ---- port-specific hooks (called by ModelBase; keep host work out of _forward) ---------------------------
    def _build(self) -> None:
        """Weights (``reference.weights.BEVFormerWeights``: the mirror's safetensors, BN folded in float64) ->
        device-form parameters (``tt.params``) -> the device graph ``tt.model.TtBEVFormer`` (TraceRunner variants
        ``image`` and ``bev``, persistent inputs / RT-dev params / states allocated here; no capture: ``_warm_one``)
        and the host state (per-stream temporal state, calibration cache)."""
        self._build_host()
        from .reference.weights import WEIGHTS_FILE, BEVFormerWeights, find_weights
        from .tt.model import TtBEVFormer
        from .tt.params import BEVFormerParams

        path = find_weights(self.weights_path)
        if path is None:
            raise FileNotFoundError(f"{WEIGHTS_FILE} not found in {self.weights_path}")
        self.net_weights = BEVFormerWeights(path)
        self.params = BEVFormerParams(self.net_weights)
        streams = int(self.knobs.MAX_STREAMS)
        self.tt = TtBEVFormer(self.device, self.params,
                              **tt_model_kwargs(self.knobs, banks=streams if streams > 1 else 0,
                                                stem_in="kpre" if self._kpre() else None,
                                                img_cams=self._upload_pipe()))
        self.runner = self.tt.runner

    def _kpre(self) -> bool:
        """STEM_IN=kpre applies (the device pre-processing needs the plain stem and numba for the node resize)."""
        from .host import fast as hfast

        return str(self.knobs.STEM_IN) == "kpre" and str(self.knobs.STEM) == "plain" and hfast.available()

    def _upload_pipe(self) -> bool:
        """UPLOAD_PIPE applies (per-camera image inputs pre-processed while the cameras before them upload)."""
        from .host import fast as hfast

        k = self.knobs
        return (bool(k.UPLOAD_PIPE) and str(k.HOST_PRE) == "bf16" and hfast.available() and str(k.STEM) == "plain"
                and str(k.UPLOAD) == "rows" and str(k.STEM_IN) == "kin")

    def _build_host(self) -> None:
        """The host half of the model: knobs, per-stream temporal states and the calibration cache (no device, no
        weights; the host tests call it directly)."""
        params = {k.upper(): v for k, v in getattr(self, "compile_params", {}).items() if k.upper() in KNOBS.knobs}
        self.knobs = KNOBS.read(**params)
        if int(self.knobs.MAX_STREAMS) < 1:
            raise ValueError("BEVFORMER_MAX_STREAMS must be >= 1")
        from .tt.msda import site_modes

        site_modes(str(self.knobs.MSDA))             # an invalid BEVFORMER_MSDA spec fails here, before the device
        self._temporal: Dict[str, Any] = {}          # stream id -> host.temporal.TemporalState
        self._geometry: Dict[bytes, Any] = {}        # lidar2img bytes -> per-calibration geometry (small LRU)
        self._cal_key: Optional[bytes] = None        # the calibration whose tables the device inputs hold
        self.streams = None                          # ttaw.trace.StreamBanks, created after the captures
        self._pipe_init(int(self.knobs.PIPELINE))
        if str(self.knobs.HOST_MALLOC) == "keep":
            from .host.fast import keep_heap

            keep_heap()

    def _warm_one(self, variant: Any) -> None:
        """Capture every trace variant (warm-up + strict capture), create the per-stream state banks, then run one
        synthetic frame through the whole host + device path so the first real call is as fast as the later ones;
        the warm-up stream is forgotten."""
        from .tt.warmup import WARMUP_STREAM, synthetic_request
        from .ttaw.trace import StreamBanks

        self.runner.capture()
        if self.pipeline == 2 and self.runner.num_command_queues != 2:
            import warnings

            warnings.warn("BEVFORMER_PIPELINE=2 needs a device opened with 2 command queues (BEVFORMER_NUM_CQS=2); "
                          "running PIPELINE=1", RuntimeWarning)
            self._pipe_init(1)
        self.streams = StreamBanks(self.runner, ["prev_bev"], max_streams=int(self.knobs.MAX_STREAMS),
                                   on_full="evict", max_gap_s=float(self.knobs.MAX_GAP_S))
        with self._lock:
            prepared = self._prepare(**synthetic_request())
            raw = self._forward(prepared)
            self._postprocess(raw, prepared, self.validate_params({}))
            self.streams.forget(WARMUP_STREAM)
            self._temporal.pop(WARMUP_STREAM, None)
        if self._kpre() and str(self.knobs.HOST_RESIZE) == "fast":
            from .host import fast as hfast

            # HOST_RESIZE=fast: the 1920x1080 kernel compiles in the background (~6 s; numba's compiler lock), the
            # rows kernel serves that size until it is ready
            hfast.warm_node_resize(((1080, 1920),))

    def _geometry_for(self, lidar2img: np.ndarray) -> Dict[str, Any]:
        """Per-calibration host geometry (``host.geometry``: float64 projection, SCA rebatching tables) and the
        device inputs built from it, cached by the bytes of ``lidar2img`` (a static rig computes them once).
        A rig whose cameras see more BEV cells than the static SCA capacity is refused (HTTP 400; PORT_LOG.md open
        question 4)."""
        from .host import geometry as hgeo
        from .tt.model import calibration_inputs

        key = np.ascontiguousarray(lidar2img, np.float32).tobytes()
        geo = self._geometry.pop(key, None)
        if geo is None:
            ref_cam, valid = hgeo.point_sampling(lidar2img)
            try:
                tables = hgeo.sca_tables(valid)
            except hgeo.SCAOverflow as e:
                raise tio.InputError(f"calibration not supported: {e}") from None
            geo = {"ref_cam": ref_cam, **tables}
            geo["inputs"] = calibration_inputs(geo)
            geo["key"] = key
        self._geometry[key] = geo                    # most recent last
        while len(self._geometry) > 8:
            self._geometry.pop(next(iter(self._geometry)))
        return geo

    def _prepare(self, points: Any = None, *, images: Any = None, calibration: Optional[Mapping[str, Any]] = None,
                 stream: Optional[Mapping[str, Any]] = None, ego: Optional[Mapping[str, Any]] = None,
                 **_: Any) -> Dict[str, Any]:
        """Six images + calibration + stream (+ ``ego`` dynamics) -> the host values of one frame with the node's
        semantics (``tt_bevformer.host``): decode + node transport + C14 preset -> the network input; lidar2img ->
        the per-calibration geometry (cached); the stream's history (device banks + host can_bus state: a new id,
        ``stream.reset`` or a time gap restart it with ``use_prev_bev = 0``) -> can_bus, TSA shift, rotation map.
        Two halves: :meth:`_prepare_host` (stateless per frame) and :meth:`_prepare_stream` (the stream history;
        with ``PIPELINE`` it runs in the device stage, in frame order)."""
        host = self._prepare_host(points, images=images, calibration=calibration, stream=stream, ego=ego)
        try:
            return self._prepare_stream(host)
        except BaseException:
            self._release_staging(host)
            raise

    def _prepare_host(self, points: Any = None, *, images: Any = None,
                      calibration: Optional[Mapping[str, Any]] = None, stream: Optional[Mapping[str, Any]] = None,
                      ego: Optional[Mapping[str, Any]] = None, **_: Any) -> Dict[str, Any]:
        """The first half of :meth:`_prepare`: request parsing, the image pre-processing of the active path and the
        per-calibration geometry; the stream fields are validated here (client errors before the device stage).
        Touches no device state."""
        from .host.inputs import prepare_request
        from .host.preprocess import preprocess_images
        from .host.temporal import can_bus_from_transform

        if points is not None:
            raise tio.InputError("this model takes six camera images, not a point cloud")
        from .host import fast as hfast

        pre = str(self.knobs.HOST_PRE)
        use_fast = (pre in ("numba", "bf16") or self._kpre()) and hfast.available()
        frame = prepare_request(images, calibration, stream, ego, bgr=not use_fast)
        can_bus_abs = frame.can_bus_abs
        if str(self.knobs.CAN_BUS_LAYOUT) != "training":
            T = frame.stream.get("T_world_from_ego")
            T = np.eye(4) if T is None else tio.parse_transform(T, field="stream.T_world_from_ego")
            can_bus_abs = can_bus_from_transform(T, dict(ego or {}), layout=str(self.knobs.CAN_BUS_LAYOUT))
        stem = str(self.knobs.STEM)
        rows = None
        pipe = self._upload_pipe()
        kpre = self._kpre()
        node_u8 = None
        if kpre:          # STEM_IN=kpre: only the node resize (other sizes) on the host; the rest on the device
            stage = self._staging_set()
            node_u8 = hfast.node_images(frame.images_bgr, threads=int(self.knobs.HOST_THREADS),
                                        out=None if stage is None else [b.view for b in stage],
                                        kernel=str(self.knobs.HOST_RESIZE))
            rows = image = None
        elif pipe:        # UPLOAD_PIPE: _forward pre-processes the cameras while it uploads them
            rows = image = None
        elif use_fast and pre == "bf16" and stem != "s2d":   # the bf16 upload rows in one pass (uint16 bits)
            rows = hfast.preprocess_rows_bf16(frame.images_bgr, rgb=True, threads=int(self.knobs.HOST_THREADS))
            image = None
        elif use_fast:   # the upload rows straight from the RGB images (NHWC: image is their NCHW view, no copy)
            rows = hfast.preprocess_rows(frame.images_bgr, rgb=True, threads=int(self.knobs.HOST_THREADS),
                                         layout="s2d" if stem == "s2d" else "nhwc")
            image = None if stem == "s2d" else rows.transpose(0, 3, 1, 2)[None]
        else:
            image = preprocess_images(frame.images_bgr, workers=int(self.knobs.HOST_THREADS))
        geo = self._geometry_for(frame.lidar2img)
        st = dict(stream or {})
        sid = str(st.get("id", "default"))
        reset = st.get("reset", False)
        if not isinstance(reset, bool):
            raise tio.InputError("stream.reset must be a boolean")
        ts = st.get("timestamp_s")
        if ts is not None:
            try:
                ts = float(ts)
            except (TypeError, ValueError):
                raise tio.InputError("stream.timestamp_s must be a number") from None
        return {"image": image, "geometry": geo, "stream_id": sid, "reset": reset, "timestamp_s": ts,
                "can_bus_abs": can_bus_abs, "rows": rows, "node_u8": node_u8, "pipe": pipe, "stem": stem,
                "images_bgr": frame.images_bgr if pipe else None, "staging": stage if kpre else None}

    def _staging_set(self) -> Optional[list]:
        """HOST_STAGING: a free set of six persistent uint8 ttnn host buffers (``ttaw.tensors.HostStaging``) from a
        pool that grows to at most 4 sets; None (the ``from_torch`` path) when off, without a device (host tests),
        when all sets are in flight or when the buffers have no zero-copy view. Never blocks. A set returns to the
        pool once its frame's upload was enqueued (:meth:`_release_staging`); a set whose frame never uploads
        (e.g. a failed call) is simply dropped."""
        if not bool(self.knobs.HOST_STAGING) or getattr(self, "runner", None) is None:
            return None
        import queue

        pool = getattr(self, "_stage_pool", None)
        if pool is False:
            return None
        if pool is None:
            pool = self._stage_pool = queue.Queue()
            self._stage_sets = 0
        try:
            return pool.get_nowait()
        except queue.Empty:
            pass
        if self._stage_sets >= 4:
            return None
        from .reference import config as C
        from .tt.model import NODE_U8_SHAPE
        from .ttaw.tensors import HostStaging

        bufs = [HostStaging(NODE_U8_SHAPE, "uint8") for _ in range(C.NUM_CAMS)]
        if not all(b.zero_copy for b in bufs):
            self._stage_pool = False
            return None
        self._stage_sets += 1
        return bufs

    def _release_staging(self, host: Dict[str, Any]) -> None:
        stage = host.pop("staging", None) if isinstance(host, dict) else None
        if stage is not None:
            self._stage_pool.put(stage)

    def _prepare_stream(self, host: Dict[str, Any]) -> Dict[str, Any]:
        """The second half of :meth:`_prepare`: select the stream (device banks), step its host can_bus state and
        build the runner's frame inputs. Enqueues device copies on a stream switch / reset, so with ``PIPELINE`` it
        runs in the device stage after the previous frame's replays were enqueued."""
        from .host import geometry as hgeo
        from .host.temporal import TemporalState
        from .tt.model import frame_inputs

        image, geo, sid, stem = host["image"], host["geometry"], host["stream_id"], host["stem"]
        reset, ts, can_bus_abs, pipe = host["reset"], host["timestamp_s"], host["can_bus_abs"], host["pipe"]
        rows, node_u8 = host["rows"], host["node_u8"]
        out: Dict[str, Any] = {"image": image, "geometry": geo, "stream_id": sid, "staging": host.get("staging")}
        if self.streams is None:                     # host tests: no device state (the host values only)
            fresh = reset or sid not in self._temporal
        else:
            fresh = self.streams.select(sid, reset=reset, timestamp_s=ts)
            for old in [k for k in self._temporal if k not in self.streams.streams]:
                self._temporal.pop(old)
        state = self._temporal.setdefault(sid, TemporalState())
        can_bus, use = state.step(can_bus_abs, reset=fresh)
        shift = hgeo.tsa_shift(can_bus, str(self.knobs.SHIFT_MODE))
        rot = hgeo.rotation_index_map(float(can_bus[17]))
        up = frame_inputs(image, can_bus=can_bus, use_prev_bev=float(use[0]), shift=shift, rot_index=rot, stem=stem,
                          img_rows=rows, wide=str(self.knobs.UPLOAD) == "rows", img_cams=pipe, node_u8=node_u8)
        if host.get("staging") is not None:          # HOST_STAGING: the host tensors that hold the images already
            for i, b in enumerate(host["staging"]):
                up["inputs"][f"img{i}"] = b.tensor
        if pipe:
            out["pending_images"] = list(host["images_bgr"])
        out.update({"upload": up, "can_bus": can_bus, "use_prev_bev": use, "fresh": bool(fresh)})
        return out

    def _forward(self, prepared: Dict[str, Any]) -> Any:
        """Upload the frame (and the calibration tables when the calibration changed), replay ``image`` then
        ``bev`` back to back, read the packed last-layer outputs (one D2H). Returns ``outputs_classes`` /
        ``outputs_coords`` ``[1, 1, 900, 10]`` and the stage times."""
        import time

        up = prepared["upload"]
        inputs = dict(up["inputs"])
        geo = prepared["geometry"]
        if geo["key"] != self._cal_key:
            inputs.update(geo["inputs"])
        runner = self.runner
        t0 = time.perf_counter()
        try:
            runner.upload(inputs=inputs, params=up["params"])
        finally:
            self._release_staging(prepared)
        if prepared.get("pending_images") is not None:
            self._upload_cameras(prepared["pending_images"])
        self._cal_key = geo["key"]
        bev = self.tt.bev_variant(up["params"]["use"])
        runner.replay("image")
        runner.replay(bev)
        packed = runner.read(bev)
        t1 = time.perf_counter()
        out = self.tt.unpack(packed)
        out["timing_ms"] = {"device_run": (t1 - t0) * 1e3}
        return out

    # ---- pipelined calls (PIPELINE, OPT_PLAN R5-3) ---------------------------------------------------------
    def _pipe_init(self, mode: int) -> None:
        """Stage state of ``PIPELINE``: frames get a ticket when their host half is done (under ``_pre_lock``, so
        in pre-processing order) and take the device in ticket order (``_turn``); ``_img_turn`` is the last ticket
        whose image replay is enqueued (a CQ1 upload of the next frame's cameras waits for it). Every ttnn call runs
        under the model lock ``_lock``; only the waits for the device (``ttnn.event_synchronize``, which releases
        the GIL) run outside it."""
        import threading

        self.pipeline = int(mode)
        self.pipelined = self.pipeline > 0
        self._pre_lock = threading.Lock()
        self._cv = threading.Condition()
        self._ticket = 0          # next ticket
        self._turn = 0            # ticket whose device stage may run
        self._img_turn = -1       # last ticket whose image replay is enqueued (or that gave up its turn)
        self._waiting = 0         # threads in a device wait outside the model lock
        self._img_done = None     # CQ0 event after the last enqueued image replay (PIPELINE=2)

    def _wait_for(self, pred) -> None:
        with self._cv:
            while not pred():
                if getattr(self, "_closed", True):
                    raise RuntimeError(f"{type(self).__name__} is closed")
                self._cv.wait(0.5)

    def _host_images(self, host: Dict[str, Any]) -> Dict[str, Any]:
        """The camera inputs of a frame as ttnn host tensors (the 26 MB uint8 copy into ttnn's host storage,
        3-4 ms), built in the pre-processing stage so that the device stage only uploads them."""
        if host.get("node_u8") is None:
            return {}
        if host.get("staging") is not None:          # HOST_STAGING: already ttnn host tensors
            return {f"img{i}": b.tensor for i, b in enumerate(host["staging"])}
        import torch

        from .reference import config as C
        from .tt.model import NODE_U8_SHAPE
        from .ttaw.tensors import to_host_tensor

        return {f"img{i}": to_host_tensor(torch.from_numpy(np.ascontiguousarray(a, np.uint8).reshape(NODE_U8_SHAPE)),
                                          "uint8")
                for i, a in enumerate(host["node_u8"][: C.NUM_CAMS])}

    def _upload_images_cq1(self, imgs: Dict[str, Any]) -> Any:
        """PIPELINE=2: write the camera inputs on CQ1 once the previous frame's image replay has read them (CQ1
        waits for that replay's event on the device; the host returns as soon as the data is queued). Returns the
        CQ1 event the next image replay waits for. Called under the model lock."""
        import ttnn

        if self._img_done is not None:
            ttnn.wait_for_event(1, self._img_done)
        for name, host in imgs.items():
            ttnn.copy_host_to_device_tensor(host, self.tt.inputs[name], cq_id=1)
        return ttnn.record_event(self.device, 1)

    def _device_stage(self, host: Dict[str, Any], imgs: Dict[str, Any], img_event: Any, ticket: int) -> Any:
        """One frame's device stage with ``PIPELINE``: stream selection + frame inputs, uploads (the cameras
        already on CQ1 when ``img_event`` is given), both replays and a non-blocking read of the bev outputs into
        fresh host tensors, all enqueued on CQ0 under the model lock; the next frame gets its turns as soon as this
        frame's image replay (``_img_turn``: its CQ1 camera upload may follow) and read-back (``_turn``) are
        enqueued, so its replays queue right behind this frame's on the device. Then the wait outside the lock.
        Returns ``(prepared, raw)``."""
        import time

        import ttnn

        from .ttaw.tensors import to_numpy
        from .ttaw.trace import _rebuild

        runner = self.runner
        with self._lock:
            self._check_open()
            t0 = time.perf_counter()
            prepared = self._prepare_stream(host)
            up = prepared["upload"]
            inputs = dict(up["inputs"])
            inputs.update(imgs)                       # ttnn host tensors (to_host_tensor passes them through)
            if img_event is not None:
                for name in imgs:
                    inputs.pop(name, None)
            geo = prepared["geometry"]
            if geo["key"] != self._cal_key:
                inputs.update(geo["inputs"])
            try:
                runner.upload(inputs=inputs, params=up["params"])
            finally:
                self._release_staging(host)
            if prepared.get("pending_images") is not None:   # STEM_IN=kin + UPLOAD_PIPE
                self._upload_cameras(prepared["pending_images"])
            if img_event is not None:
                ttnn.wait_for_event(0, img_event)
            self._cal_key = geo["key"]
            runner.replay("image")
            if self.pipeline == 2:
                self._img_done = ttnn.record_event(self.device, 0)
            with self._cv:
                self._img_turn = max(self._img_turn, ticket)
                self._cv.notify_all()
            bev = self.tt.bev_variant(up["params"]["use"])
            runner.replay(bev)
            # the read-back enqueued now (CQ0, behind the replay; before the next frame's replays can overwrite
            # the outputs) into this frame's own host tensors
            trace = runner._trace_for(bev, runner._last_phase[bev])
            hosts = [ttnn.allocate_tensor_on_host(t.spec, self.device) for _, t, _ in trace.leaves]
            for (_, t, _), h in zip(trace.leaves, hosts):
                ttnn.copy_device_to_host_tensor(t, h, blocking=False, cq_id=0)
            done = ttnn.record_event(self.device, 0)
            with self._cv:
                self._waiting += 1
                self._turn = max(self._turn, ticket + 1)
                self._cv.notify_all()
        try:
            ttnn.event_synchronize(done)              # releases the GIL: the next frame's host half runs meanwhile
        finally:
            with self._cv:
                self._waiting -= 1
                self._cv.notify_all()
        t1 = time.perf_counter()
        values = {}
        for (path, _, layout), h in zip(trace.leaves, hosts):
            arr = to_numpy(h)
            values[path] = layout.unpack(arr) if layout is not None else arr
        structure = trace.outputs
        from .ttaw.trace import Packed

        if isinstance(structure, Packed) or not isinstance(structure, (Mapping, list, tuple)):
            packed = values[()]
        else:
            packed = _rebuild(structure, values)
        raw = self.tt.unpack(packed)
        raw["timing_ms"] = {"device_run": (t1 - t0) * 1e3}
        return prepared, raw

    def __call__(self, points: Any = None, *, sweeps: Any = None, images: Any = None,
                 calibration: Optional[Mapping[str, Any]] = None, stream: Optional[Mapping[str, Any]] = None,
                 inputs: Any = None, **params: Any):
        """One frame (``ModelBase.__call__``). With ``BEVFORMER_PIPELINE`` (OPT_PLAN R5-3) concurrent calls overlap:
        the host half of the pre-processing (and the image host tensors) under ``_pre_lock``, then the device stage
        in the same frame order (the stream history steps there, so a stream's frames keep their order), the
        post-processing under no lock. ``PIPELINE=2`` also uploads the cameras on CQ1 while the previous frame's
        bev trace runs. Same outputs as serial calls in the same frame order; ``timing_ms`` adds ``wait`` (time
        queued for a stage)."""
        if not getattr(self, "pipelined", False):
            return super().__call__(points, sweeps=sweeps, images=images, calibration=calibration, stream=stream,
                                    inputs=inputs, **params)
        import time

        self._check_open()
        extra = {k: params.pop(k) for k in list(params) if k in self.EXTRA_INPUTS}
        p = self.validate_params(params)
        t0 = time.perf_counter()
        with self._pre_lock:
            self._check_open()
            t1 = time.perf_counter()
            host = self._prepare_host(points, images=images, calibration=calibration, stream=stream, **extra)
            try:
                imgs = self._host_images(host)
            except BaseException:
                self._release_staging(host)
                raise
            ticket = self._ticket
            self._ticket += 1
            t2 = time.perf_counter()
        try:
            img_event = None
            if self.pipeline == 2 and imgs:
                self._wait_for(lambda: self._img_turn >= ticket - 1)
                with self._lock:
                    self._check_open()
                    img_event = self._upload_images_cq1(imgs)
                self._release_staging(host)
            t3 = time.perf_counter()
            self._wait_for(lambda: self._turn == ticket)
            t4 = time.perf_counter()
            prepared, raw = self._device_stage(host, imgs, img_event, ticket)
            t5 = time.perf_counter()
        finally:
            self._release_staging(host)               # (no-op once the upload released it)
            with self._cv:                            # a failed frame gives up its turns too
                # ... but only once the frames before it had theirs: a frame that failed before its device turn
                # (e.g. the CQ1 upload) must not advance _turn past a frame still in its device stage
                while self._turn < ticket and not getattr(self, "_closed", True):
                    self._cv.wait(0.5)
                self._img_turn = max(self._img_turn, ticket)
                self._turn = max(self._turn, ticket + 1)
                self._cv.notify_all()
        out = self._postprocess(raw, prepared, p)
        t6 = time.perf_counter()
        out.timing_ms.update({"preprocess": (t2 - t1) * 1e3, "upload_cq1": (t3 - t2) * 1e3, "device": (t5 - t4) * 1e3,
                              "postprocess": (t6 - t5) * 1e3, "wait": ((t1 - t0) + (t4 - t3)) * 1e3,
                              "total": (t6 - t0) * 1e3})
        return out

    predict = __call__

    def image_rows(self, images: Any, on_camera: Any = None) -> np.ndarray:
        """Six RGB uint8 node-order images -> the bf16 upload rows (uint16 bits ``[6, 736, 3840]``,
        ``host.fast.preprocess_rows_bf16``) in a buffer reused across frames."""
        from .host import fast as hfast
        from .reference import config as C

        buf = getattr(self, "_rows_buf", None)
        if buf is None:
            buf = self._rows_buf = np.empty((C.NUM_CAMS, C.IMG_HW[0], C.IMG_HW[1] * 3), np.uint16)
        return hfast.preprocess_rows_bf16(images, rgb=True, threads=int(self.knobs.HOST_THREADS), out=buf,
                                          on_camera=on_camera)

    def _upload_cameras(self, images: Any) -> None:
        """UPLOAD_PIPE: a worker thread pre-processes the cameras one after the other (numba, nogil) while this
        thread uploads each camera's rows as soon as they are written (the H2D of camera i overlaps camera i + 1)."""
        import queue
        import threading

        import torch

        from .reference import config as C
        from .tt.model import CAM_ROWS_SHAPE

        q: "queue.Queue" = queue.Queue()

        def work() -> None:
            try:
                self.image_rows(images, on_camera=lambda i, r: q.put((i, r)))
            except BaseException as e:  # noqa: BLE001 -- re-raised in the calling thread
                q.put((-1, e))

        th = threading.Thread(target=work, name="bevformer-pre", daemon=True)
        th.start()
        try:
            for _ in range(C.NUM_CAMS):
                i, r = q.get()
                if i < 0:
                    raise r
                t = torch.from_numpy(r.view(np.int16)).view(torch.bfloat16).reshape(CAM_ROWS_SHAPE)
                self.runner.upload(inputs={f"img{i}": t})
        finally:
            th.join()

    def _postprocess(self, raw: Any, prepared: Dict[str, Any], params: Dict[str, Any]) -> Output:
        """The node's post-processing (``host.postprocess``: last decoder layer, sigmoid, top ``max_num``, score >
        ``score_threshold``, ``post_center_range``, no NMS) -> ``Detections3D`` in LIDAR_TOP with the Autoware labels;
        ``raw`` holds ``outputs_classes`` / ``outputs_coords`` (the last layer suffices); ``meta.stream`` says which
        history the frame used (``use_prev_bev``)."""
        from .host import postprocess as hpost

        dets = hpost.decode(raw["outputs_classes"], raw["outputs_coords"], score_threshold=params["score_threshold"],
                            max_num=params["max_num"])
        out = hpost.to_output(dets, model=self.MODEL_NAME, physical=params["physical_boxes"])
        if isinstance(prepared, dict) and "stream_id" in prepared:
            out.meta["stream"] = {"id": prepared["stream_id"], "fresh": bool(prepared.get("fresh", False)),
                                  "use_prev_bev": float(np.asarray(prepared.get("use_prev_bev", [0.0])).reshape(-1)[0])}
        out.timing_ms.update({k: round(float(v), 3) for k, v in (raw.get("timing_ms") or {}).items()})
        return out

    def _release(self) -> None:
        """Release the traces, the persistent device tensors and the device weights; also called when
        ``from_pretrained`` fails half-way. With ``PIPELINE`` it first waits (up to 60 s) for the device waits of
        frames still in flight (``close()`` has marked the model closed, so they stop at their next stage)."""
        import time

        cv = getattr(self, "_cv", None)
        if cv is not None:
            end = time.monotonic() + 60.0
            with cv:
                while self._waiting > 0 and time.monotonic() < end:
                    cv.wait(0.5)
        tt = getattr(self, "tt", None)
        if tt is not None:
            tt.release()
        else:
            runner = getattr(self, "runner", None)
            if runner is not None:
                runner.release()

    def extra_info(self) -> Dict[str, Any]:
        """Additions to ``model.info`` and ``/info``: the trace variants, CQs and persistent tensors of the runner,
        the load-time knobs and the stream banks."""
        info: Dict[str, Any] = {}
        runner = getattr(self, "runner", None)
        if runner is not None:
            info["trace"] = runner.describe()
        knobs = getattr(self, "knobs", None)
        if knobs is not None:
            info["knobs"] = knobs.as_dict()
        streams = getattr(self, "streams", None)
        if streams is not None:
            info["streams"] = streams.describe()
        return info