-
Notifications
You must be signed in to change notification settings - Fork 55
Expand file tree
/
Copy pathgroot_thor.py
More file actions
2235 lines (1916 loc) · 109 KB
/
Copy pathgroot_thor.py
File metadata and controls
2235 lines (1916 loc) · 109 KB
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
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""FlashRT — GrootTorchFrontendThor: GROOT N1.6 inference via flash_rt_kernels.so.
Architecture: Eagle3-VL (SigLIP2 + Qwen3-1.7B) + AlternateVLDiT (32L, 4 flow-matching steps)
Usage:
pipe = GrootTorchFrontendThor("/path/to/groot/checkpoint", num_views=2)
pipe.set_prompt("pick up the red block")
result = pipe.infer({"image": img1, "wrist_image": img2})
actions = result["actions"] # (16, action_dim) numpy
"""
import ctypes
import json
import logging
import math
import pathlib
import time
from typing import Optional
import numpy as np
import torch
import torch.nn.functional as F
from flash_rt.hardware.thor.shared_primitives import siglip_forward # SigLIP2 == Pi0.5 SigLIP
from flash_rt.hardware.thor.attn_backend_groot import (
ThorGrootAttnBackend,
make_groot_attention_spec,
)
from flash_rt.models.groot.pipeline_thor import (
siglip2_forward,
eagle_project,
qwen3_forward,
dit_forward,
embodiment_encode_state,
embodiment_encode_action,
embodiment_decode_action,
)
import flash_rt.flash_rt_kernels as fvk
from flash_rt.core.quant.calibrator import load_calibration, save_calibration
logger = logging.getLogger(__name__)
fp16 = torch.float16
bf16 = torch.bfloat16
fp8 = torch.float8_e4m3fn
# ★ GROOT uses fp16 throughout (not bf16) — Thor fp16 has better perf ★
# SigLIP2/Qwen3: FP8 GEMM → fp16 intermediates (same as Pi0.5)
# DiT/mlp1/Embodiment: fp16 GEMM (fp16_nn) + fp16 kernels
# Kernel dtype rules:
# layer_norm_fp16 = fp16, layer_norm = bf16 (do not mix)
# gelu_inplace_fp16 = fp16, gelu_inplace = bf16
# add_bias_fp16 = fp16 only
# softmax_fp16 = fp16 (cols must be aligned, otherwise use PyTorch SDPA)
COMPUTE_DTYPE = fp16
# Embodiment tag → projector index. Shared with rtx — see
# flash_rt/hardware/groot_embodiments.py. Same 32-slot per-embodiment
# MLP layout; only a subset of slots have trained weights in the
# GR00T-N1.6-3B base checkpoint (see TRAINED_EMBODIMENT_IDS).
from flash_rt.models.groot.embodiments import (
EMBODIMENT_TAG_TO_INDEX,
PUBLIC_TRAINED_TAGS,
is_embodiment_trained,
)
# ── Checkpoint key prefixes (C2: double vision_model!) ──
VIS_PREFIX = "backbone.model.vision_model.vision_model" # ★ double vision_model ★
LLM_PREFIX = "backbone.model.language_model.model"
MLP1_PREFIX = "backbone.model.mlp1"
DIT_PREFIX = "action_head.model"
AH_PREFIX = "action_head"
from flash_rt.core.thor_frontend_utils import quant_fp8 # noqa: E402
class GrootTorchFrontendThor:
"""GROOT N1.6 inference pipeline on Thor SM110."""
def __init__(self, checkpoint, num_views=2, autotune=3,
embodiment_tag="new_embodiment", use_fp8=True):
"""Initialize GROOT pipeline.
Args:
checkpoint: path to GROOT safetensors checkpoint directory
num_views: camera views (default 2)
autotune: CUDA Graph autotune intensity (0=off, 3=default)
embodiment_tag: target embodiment for per-embodiment MLPs
"""
if embodiment_tag not in EMBODIMENT_TAG_TO_INDEX:
raise ValueError(
f"Unknown embodiment_tag {embodiment_tag!r}. "
f"Known tags: {sorted(EMBODIMENT_TAG_TO_INDEX.keys())}. "
f"Trained in GR00T-N1.6-3B: {PUBLIC_TRAINED_TAGS}.")
self._checkpoint_path = pathlib.Path(checkpoint)
self._num_views = num_views
self._autotune = autotune
self.use_fp8 = bool(use_fp8)
self._embodiment_tag = embodiment_tag
self._embodiment_id = EMBODIMENT_TAG_TO_INDEX[embodiment_tag]
self._real_data_calibrated = False
self.calibrated = False
logger.info("GROOT pipeline init: checkpoint=%s, views=%d, embodiment=%s (id=%d)",
checkpoint, num_views, embodiment_tag, self._embodiment_id)
if not is_embodiment_trained(embodiment_tag):
logger.warning(
"embodiment_tag=%r (id=%d) is NOT trained in the GR00T-N1.6-3B "
"base checkpoint — per-embodiment MLP weights are at "
"initialization and the model will emit noise-like actions. "
"Pick one of %s for a demo, or fine-tune this slot before "
"deployment.",
embodiment_tag, self._embodiment_id, PUBLIC_TRAINED_TAGS,
)
# ── Init kernels ──
self._gemm = fvk.GemmRunner()
self._ctx = fvk.FvkContext()
# ── Load FMHA ──
fmha_path = pathlib.Path(fvk.__file__).parent / "libfmha_fp16_strided.so"
if fmha_path.exists():
fvk.load_fmha_library(str(fmha_path))
fvk.load_fmha_strided_library(str(fmha_path))
logger.info("FMHA loaded: standard=%s, strided=OK", fvk.has_cutlass_fmha())
# ── Model dimensions (verified from checkpoint 2026-04-04) ──
# SigLIP2 (27 layers, full attention, no RoPE)
self.D_sig = 1152
self.NH_sig = 16
self.HD_sig = 72
self.H_sig = 4304
self.L_sig = 27
self.spv_raw = 256 # raw patches per view (before pixel unshuffle)
self.spv = 64 # tokens per view after 2x2 pixel_unshuffle (C4)
self.mlp1_in = 4608 # 1152 * 4 after pixel unshuffle (C5)
# Qwen3 (★16 layers★, select_layer=16, checkpoint truncated) (C1)
self.D_llm = 2048
self.NHQ = 16
self.NHKV = 8
self.HD_llm = 128
self.H_llm = 6144
self.L_llm = 16 # ★ NOT 28 — checkpoint only has 16 layers ★
self.QKV_DIM = self.NHQ * self.HD_llm + 2 * self.NHKV * self.HD_llm # 4096
# DiT (32 layers, ★GELU not GEGLU★) (C3)
self.D_dit = 1536
self.NH_dit = 32
self.HD_dit = 48
self.H_dit = 6144 # ★ GELU FFN dim, NOT 2x for GEGLU ★
self.L_dit = 32
self.output_dim = 1024
# Action (★padded max dims★) (C6, C12)
self.action_dim = 128 # ★ NOT 29 — padded max, per-embodiment actual varies ★
self.state_dim = 128 # ★ NOT 29 ★
self.action_horizon = 50 # ★ NOT 16 — padded max ★
self.num_steps = 4 # flow-matching Euler steps
self.Sa = self.action_horizon + 1 # 51 = 1 state + 50 actions
# ── Load checkpoint (keep full sd; SigLIP init deferred to _capture_all_graphs) ──
self._load_checkpoint()
logger.info("GROOT pipeline ready. GPU mem: %.1fGB free",
torch.cuda.mem_get_info()[0] / 1e9)
# ─────────────────────────────────────────────────────────────
# Weight loading
# ─────────────────────────────────────────────────────────────
def _load_checkpoint(self):
"""Load checkpoint to GPU. SigLIP init deferred to _capture_all_graphs."""
from safetensors import safe_open
ckpt_path = self._checkpoint_path
st_files = sorted(ckpt_path.glob("*.safetensors"))
if not st_files:
raise FileNotFoundError(f"No safetensors found in {ckpt_path}")
logger.info("Loading %d safetensors files...", len(st_files))
state_dict = {}
for f in st_files:
with safe_open(str(f), framework="pt", device="cuda") as sf:
for key in sf.keys():
state_dict[key] = sf.get_tensor(key)
logger.info("Loaded %d tensors", len(state_dict))
# Extract embeddings (needed for set_prompt)
self._qwen3_embed = state_dict[f"{LLM_PREFIX}.embed_tokens.weight"]
self._vlln_w = state_dict["action_head.vlln.weight"]
self._vlln_b = state_dict["action_head.vlln.bias"]
# Keep full sd (SigLIP + Qwen3 + DiT all initialized in _capture_all_graphs)
self._full_sd = state_dict
def _load_siglip2_weights(self, sd):
"""Load SigLIP2 vision encoder weights.
Key prefix: backbone.model.vision_model.vision_model.encoder.layers.{i}
(C2: double vision_model in key path!)
Structure per layer: LayerNorm → QKV merged → FMHA → O → LN → FFN up → FFN down
All weights FP8 quantized (Q/K/V/O, fc1, fc2).
"""
logger.info("Loading SigLIP2 weights (27 layers)...")
D = self.D_sig # 1152
H = self.H_sig # 4304
L = self.L_sig # 27
# Declarative weight-loader pass (stage 7.8, partial). Populates:
# self._sig_{ln_attn,ln_ffn,qkv,o,up,down}_{w,b} (27-layer lists)
# self._sig_alpha (108 fp32 scales)
# Qwen3 / DiT / embodiment remain inline — see _groot_thor_spec.py docstring.
from flash_rt.executors.torch_weights import DictSource, WeightLoader
from flash_rt.frontends.torch._groot_thor_spec import build_siglip2_spec
WeightLoader(source=DictSource(sd), target=self,
spec=build_siglip2_spec()).run()
# Post-layernorm (applied after SigLIP before pixel unshuffle)
self._sig_post_ln_w = sd[f"{VIS_PREFIX}.post_layernorm.weight"].to(fp16)
self._sig_post_ln_b = sd[f"{VIS_PREFIX}.post_layernorm.bias"].to(fp16)
# Patch embedding: Linear [1152, 588] (not Conv2d!) — 588 = 14*14*3
self._sig_patch_w = sd[f"{VIS_PREFIX}.embeddings.patch_embedding.weight"].to(fp16) # [1152, 588]
self._sig_patch_b = sd[f"{VIS_PREFIX}.embeddings.patch_embedding.bias"].to(fp16) # [1152]
# Position embedding: [256, 1152] — 256 patches, NO CLS (C15)
self._sig_pos_embed = sd[f"{VIS_PREFIX}.embeddings.position_embedding.weight"].to(fp16) # [256, 1152]
# mlp1: LN(4608) → Linear(4608,2048) → GELU → Linear(2048,2048) (C5)
self._mlp1_ln_w = sd[f"{MLP1_PREFIX}.0.weight"].to(fp16) # [4608]
self._mlp1_ln_b = sd[f"{MLP1_PREFIX}.0.bias"].to(fp16) # [4608]
self._mlp1_fc1_w = sd[f"{MLP1_PREFIX}.1.weight"].T.contiguous().to(fp16) # [2048,4608]→[4608,2048] for NN
self._mlp1_fc1_b = sd[f"{MLP1_PREFIX}.1.bias"].to(fp16) # [2048]
self._mlp1_fc2_w = sd[f"{MLP1_PREFIX}.3.weight"].T.contiguous().to(fp16) # [2048,2048]→[2048,2048]
self._mlp1_fc2_b = sd[f"{MLP1_PREFIX}.3.bias"].to(fp16) # [2048]
# Unit scale for FP8 casts
self._unit_scale = torch.ones(1, dtype=torch.float32, device='cuda')
# Build weight dicts (data pointers) — per-layer tensors are kept
# alive as self._sig_*_{w,b} lists by the loader above.
self._sig_weights = {
'ln_attn_w': [w.data_ptr() for w in self._sig_ln_attn_w],
'ln_attn_b': [w.data_ptr() for w in self._sig_ln_attn_b],
'qkv_w': [w.data_ptr() for w in self._sig_qkv_w],
'qkv_b': [w.data_ptr() for w in self._sig_qkv_b],
'o_w': [w.data_ptr() for w in self._sig_o_w],
'o_b': [w.data_ptr() for w in self._sig_o_b],
'ln_ffn_w': [w.data_ptr() for w in self._sig_ln_ffn_w],
'ln_ffn_b': [w.data_ptr() for w in self._sig_ln_ffn_b],
'up_w': [w.data_ptr() for w in self._sig_up_w],
'up_b': [w.data_ptr() for w in self._sig_up_b],
'down_w': [w.data_ptr() for w in self._sig_down_w],
'down_b': [w.data_ptr() for w in self._sig_down_b],
'alpha': self._sig_alpha,
'unit_scale': self._unit_scale.data_ptr(),
}
# ── FP16 reference path: raw (non-quantized) SigLIP GEMM weights ──
# ``siglip_forward(use_fp8=False)`` consumes the same dict but expects
# FP16 [K, N] qkv/o/up/down weights. Override those pointers in place.
if not self.use_fp8:
_vp = f"{VIS_PREFIX}.encoder.layers.{{i}}"
self._sig_qkv_w_fp16, self._sig_o_w_fp16 = [], []
self._sig_up_w_fp16, self._sig_down_w_fp16 = [], []
for i in range(L):
p = _vp.format(i=i)
q = sd[f"{p}.self_attn.q_proj.weight"]
k = sd[f"{p}.self_attn.k_proj.weight"]
v = sd[f"{p}.self_attn.v_proj.weight"]
self._sig_qkv_w_fp16.append(torch.cat([q, k, v], dim=0).T.contiguous().to(fp16))
self._sig_o_w_fp16.append(sd[f"{p}.self_attn.out_proj.weight"].T.contiguous().to(fp16))
self._sig_up_w_fp16.append(sd[f"{p}.mlp.fc1.weight"].T.contiguous().to(fp16))
self._sig_down_w_fp16.append(sd[f"{p}.mlp.fc2.weight"].T.contiguous().to(fp16))
self._sig_weights['qkv_w'] = [w.data_ptr() for w in self._sig_qkv_w_fp16]
self._sig_weights['o_w'] = [w.data_ptr() for w in self._sig_o_w_fp16]
self._sig_weights['up_w'] = [w.data_ptr() for w in self._sig_up_w_fp16]
self._sig_weights['down_w'] = [w.data_ptr() for w in self._sig_down_w_fp16]
logger.info("SigLIP2 weights loaded: %d layers, patch_embed [%s], pos_embed [%s], mlp1 ready",
L, list(self._sig_patch_w.shape), list(self._sig_pos_embed.shape))
def _load_qwen3_weights(self, sd):
"""Load Qwen3-1.7B weights with FP8 quantization.
Key differences from Pi0.5 Gemma:
- ★16 layers★ (select_layer=16, checkpoint already truncated) (C1)
- GQA 16Q/8KV: QKV merged = [D, 4096]
- q_norm/k_norm weights: [128] per layer
- Gate+Up merge: [D, 12288]
- QKV interleave for RoPE compatibility
"""
logger.info("Loading Qwen3 weights (%d layers)...", self.L_llm)
prefix = f"{LLM_PREFIX}.layers"
self._qwen3_w = {
'ln_attn_w': [], 'qkv_w': [], 'qkv_s': [],
'qkv_w_fp16': [], # fp16 for fp16_nn path
'q_norm_w': [], 'k_norm_w': [],
'o_w': [], 'o_s': [],
'o_w_fp16': [],
'ln_ffn_w': [], 'gate_up_w': [], 'gate_up_s': [],
'gate_up_w_fp16': [],
'down_w': [], 'down_s': [],
'down_w_fp16': [],
}
for i in range(self.L_llm):
lp = f"{prefix}.{i}"
# Input LayerNorm (RMSNorm, no bias)
self._qwen3_w['ln_attn_w'].append(sd[f"{lp}.input_layernorm.weight"].to(fp16))
# QKV: separate → merge [D, QKV_DIM]
q_w = sd[f"{lp}.self_attn.q_proj.weight"] # [2048, 2048]
k_w = sd[f"{lp}.self_attn.k_proj.weight"] # [1024, 2048]
v_w = sd[f"{lp}.self_attn.v_proj.weight"] # [1024, 2048]
# Non-interleaved QKV (for PyTorch rotate_half RoPE)
qkv_raw = torch.cat([q_w, k_w, v_w], dim=0) # [4096, 2048]
qkv_raw_T = qkv_raw.T.contiguous() # [2048, 4096]
self._qwen3_w['qkv_w_fp16'].append(qkv_raw_T.to(fp16))
qkv_fp8_raw, qkv_s_raw = quant_fp8(qkv_raw_T)
self._qwen3_w['qkv_w'].append(qkv_fp8_raw) # FP8 non-interleaved
self._qwen3_w['qkv_s'].append(qkv_s_raw)
# Interleaved QKV (for future csrc rope_apply, not used now)
# q_w_il = _interleave_qk(q_w, self.NHQ)
# k_w_il = _interleave_qk(k_w, self.NHKV)
# q_norm / k_norm weights [128]
self._qwen3_w['q_norm_w'].append(sd[f"{lp}.self_attn.q_norm.weight"].to(fp16))
self._qwen3_w['k_norm_w'].append(sd[f"{lp}.self_attn.k_norm.weight"].to(fp16))
# O projection [2048, 2048]
o_w = sd[f"{lp}.self_attn.o_proj.weight"]
o_wT = o_w.T.contiguous()
o_fp8, o_s = quant_fp8(o_wT)
self._qwen3_w['o_w'].append(o_fp8)
self._qwen3_w['o_s'].append(o_s)
self._qwen3_w['o_w_fp16'].append(o_wT.to(fp16))
# Post-attention RMSNorm
self._qwen3_w['ln_ffn_w'].append(sd[f"{lp}.post_attention_layernorm.weight"].to(fp16))
# FFN: gate_proj + up_proj → merged [D, 2H]
gate_w = sd[f"{lp}.mlp.gate_proj.weight"] # [6144, 2048]
up_w = sd[f"{lp}.mlp.up_proj.weight"] # [6144, 2048]
gate_up = torch.cat([gate_w, up_w], dim=0) # [12288, 2048]
gu_T = gate_up.T.contiguous() # [2048, 12288]
gu_fp8, gu_s = quant_fp8(gu_T)
self._qwen3_w['gate_up_w'].append(gu_fp8)
self._qwen3_w['gate_up_s'].append(gu_s)
self._qwen3_w['gate_up_w_fp16'].append(gu_T.to(fp16))
# down_proj [2048, 6144]
down_w = sd[f"{lp}.mlp.down_proj.weight"] # [2048, 6144]
dn_T = down_w.T.contiguous() # [6144, 2048]
dn_fp8, dn_s = quant_fp8(dn_T)
self._qwen3_w['down_w'].append(dn_fp8)
self._qwen3_w['down_s'].append(dn_s)
self._qwen3_w['down_w_fp16'].append(dn_T.to(fp16))
# Pre-allocate FP8 scale device tensors
self._qwen3_w['qkv_s_dev'] = [torch.tensor([s], dtype=torch.float32, device='cuda')
for s in self._qwen3_w['qkv_s']]
self._qwen3_w['gate_up_s_dev'] = [torch.tensor([s], dtype=torch.float32, device='cuda')
for s in self._qwen3_w['gate_up_s']]
# Final RMSNorm
self._qwen3_final_norm_w = sd[f"{LLM_PREFIX}.norm.weight"]
# Token embeddings (for prompt encoding)
self._qwen3_embed = sd[f"{LLM_PREFIX}.embed_tokens.weight"] # [vocab, 2048]
logger.info("Qwen3 weights loaded: %d layers, embed [%s]",
self.L_llm, list(self._qwen3_embed.shape))
def _load_dit_weights(self, sd):
"""Load AlternateVLDiT weights.
32 blocks with alternating self-attn (odd) / cross-attn (even).
Self-attn blocks: to_k/to_v input=1536
Cross-attn blocks: to_k/to_v input=2048 (from backbone)
All DiT weights stay BF16 (FP8 optimization deferred).
★ CORRECTIONS (C3, C7, C8, C9, C11): ★
- FFN is GELU, NOT GEGLU — ff.net.0.proj shape [6144, 1536] not [12288, 1536]
- norm1.norm has NO parameters (elementwise_affine=False)
- norm3 has NO parameters (elementwise_affine=False)
- norm_out has NO parameters (elementwise_affine=False)
- Output conditioning: shift, scale = chunk(2) — shift FIRST
"""
logger.info("Loading DiT weights (32 layers)...")
prefix = f"{DIT_PREFIX}.transformer_blocks"
self._dit_w = {
# AdaLayerNorm conditioning (no norm1.norm weights — C7)
'norm1_linear_w': [], 'norm1_linear_b': [],
# Attention (vary by block type) — fp16 for bias, FP8 for weights
'q_w': [], 'q_b': [], 'q_w_fp8': [], 'q_s': [],
'k_w': [], 'k_b': [], 'k_w_fp8': [], 'k_s': [],
'v_w': [], 'v_b': [], 'v_w_fp8': [], 'v_s': [],
'o_w': [], 'o_b': [], 'o_w_fp8': [], 'o_s': [],
# For self-attn blocks: merged QKV
'qkv_w': [], 'qkv_b': [], 'qkv_w_fp8': [], 'qkv_s': [],
# FFN ★GELU★ (no norm3 — C8)
'ff_up_w': [], 'ff_up_b': [], 'ff_up_w_fp8': [], 'ff_up_s': [],
'ff_down_w': [], 'ff_down_b': [], 'ff_down_w_fp8': [], 'ff_down_s': [],
}
for l in range(self.L_dit):
lp = f"{prefix}.{l}"
is_self_attn = (l % 2 == 1)
# AdaLayerNorm: only norm1.linear (conditioning projection)
# norm1.norm has NO learnable parameters (elementwise_affine=False) (C7)
# ★ All DiT weights converted to fp16 for Thor perf ★
self._dit_w['norm1_linear_w'].append(
sd[f"{lp}.norm1.linear.weight"].T.contiguous().to(fp16)) # [1536,3072]
self._dit_w['norm1_linear_b'].append(
sd[f"{lp}.norm1.linear.bias"].to(fp16)) # [3072]
# Attention projections
q_w = sd[f"{lp}.attn1.to_q.weight"] # [1536, 1536]
k_w = sd[f"{lp}.attn1.to_k.weight"] # [1536, 1536] or [1536, 2048]
v_w = sd[f"{lp}.attn1.to_v.weight"] # same
o_w = sd[f"{lp}.attn1.to_out.0.weight"] # [1536, 1536]
# fp16 weights (kept for non-FP8 path / bias)
q_wT = q_w.T.contiguous()
k_wT = k_w.T.contiguous()
v_wT = v_w.T.contiguous()
o_wT = o_w.T.contiguous()
self._dit_w['q_w'].append(q_wT.to(fp16))
self._dit_w['q_b'].append(sd[f"{lp}.attn1.to_q.bias"].to(fp16))
self._dit_w['k_w'].append(k_wT.to(fp16))
self._dit_w['k_b'].append(sd[f"{lp}.attn1.to_k.bias"].to(fp16))
self._dit_w['v_w'].append(v_wT.to(fp16))
self._dit_w['v_b'].append(sd[f"{lp}.attn1.to_v.bias"].to(fp16))
self._dit_w['o_w'].append(o_wT.to(fp16))
self._dit_w['o_b'].append(sd[f"{lp}.attn1.to_out.0.bias"].to(fp16))
# FP8 quantized weights for GEMM
q_fp8, q_s = quant_fp8(q_wT); self._dit_w['q_w_fp8'].append(q_fp8); self._dit_w['q_s'].append(q_s)
k_fp8, k_s = quant_fp8(k_wT); self._dit_w['k_w_fp8'].append(k_fp8); self._dit_w['k_s'].append(k_s)
v_fp8, v_s = quant_fp8(v_wT); self._dit_w['v_w_fp8'].append(v_fp8); self._dit_w['v_s'].append(v_s)
o_fp8, o_s = quant_fp8(o_wT); self._dit_w['o_w_fp8'].append(o_fp8); self._dit_w['o_s'].append(o_s)
# Self-attn blocks: merge QKV for single GEMM
if is_self_attn:
qkv_merged = torch.cat([q_w, k_w, v_w], dim=0) # [4608, 1536]
qkv_mT = qkv_merged.T.contiguous()
self._dit_w['qkv_w'].append(qkv_mT.to(fp16))
qkv_bias = torch.cat([sd[f"{lp}.attn1.to_q.bias"],
sd[f"{lp}.attn1.to_k.bias"],
sd[f"{lp}.attn1.to_v.bias"]], dim=0).to(fp16)
self._dit_w['qkv_b'].append(qkv_bias)
qkv_fp8, qkv_s = quant_fp8(qkv_mT)
self._dit_w['qkv_w_fp8'].append(qkv_fp8)
self._dit_w['qkv_s'].append(qkv_s)
else:
self._dit_w['qkv_w'].append(None)
self._dit_w['qkv_b'].append(None)
self._dit_w['qkv_w_fp8'].append(None)
self._dit_w['qkv_s'].append(None)
# FFN ★GELU★ (NOT GEGLU) (C3) — no norm3 params (C8)
ff_up = sd[f"{lp}.ff.net.0.proj.weight"] # [6144, 1536]
ff_down = sd[f"{lp}.ff.net.2.weight"] # [1536, 6144]
ff_up_T = ff_up.T.contiguous()
ff_down_T = ff_down.T.contiguous()
self._dit_w['ff_up_w'].append(ff_up_T.to(fp16))
self._dit_w['ff_up_b'].append(sd[f"{lp}.ff.net.0.proj.bias"].to(fp16))
self._dit_w['ff_down_w'].append(ff_down_T.to(fp16))
self._dit_w['ff_down_b'].append(sd[f"{lp}.ff.net.2.bias"].to(fp16))
fu_fp8, fu_s = quant_fp8(ff_up_T); self._dit_w['ff_up_w_fp8'].append(fu_fp8); self._dit_w['ff_up_s'].append(fu_s)
fd_fp8, fd_s = quant_fp8(ff_down_T); self._dit_w['ff_down_w_fp8'].append(fd_fp8); self._dit_w['ff_down_s'].append(fd_s)
# Output layers — norm_out has NO parameters (C9)
self._dit_proj_out_1_w = sd[f"{DIT_PREFIX}.proj_out_1.weight"].T.contiguous().to(fp16)
self._dit_proj_out_1_b = sd[f"{DIT_PREFIX}.proj_out_1.bias"].to(fp16)
self._dit_proj_out_2_w = sd[f"{DIT_PREFIX}.proj_out_2.weight"].T.contiguous().to(fp16)
self._dit_proj_out_2_b = sd[f"{DIT_PREFIX}.proj_out_2.bias"].to(fp16)
# Timestep encoder — fp16
ts_prefix = f"{DIT_PREFIX}.timestep_encoder.timestep_embedder"
self._ts_linear1_w = sd[f"{ts_prefix}.linear_1.weight"].T.contiguous().to(fp16)
self._ts_linear1_b = sd[f"{ts_prefix}.linear_1.bias"].to(fp16)
self._ts_linear2_w = sd[f"{ts_prefix}.linear_2.weight"].T.contiguous().to(fp16)
self._ts_linear2_b = sd[f"{ts_prefix}.linear_2.bias"].to(fp16)
# vlln — fp16
self._vlln_w = sd[f"{AH_PREFIX}.vlln.weight"].to(fp16)
self._vlln_b = sd[f"{AH_PREFIX}.vlln.bias"].to(fp16)
# Position embedding — fp16
self._position_embedding = sd[f"{AH_PREFIX}.position_embedding.weight"].to(fp16)
logger.info("DiT weights loaded: %d layers, pos_embed [%s]",
self.L_dit, list(self._position_embedding.shape))
def _load_embodiment_weights(self, sd):
"""Extract per-embodiment weights for target embodiment.
★ CORRECTIONS (C6, C10, C13, C14): ★
- action_dim=128, state_dim=128 (padded max, not 29)
- action_encoder hidden=1536 (not 1024)
- W layout is [in, out] — NO transpose needed for bmm(x, W) (C13)
- mask_token doesn't exist (state_dropout_prob=0.0) (C14)
CategorySpecificLinear: W=[num_categories, input_dim, hidden_dim]
Forward: output = bmm(x, W[cat_id]) + b[cat_id]
For bf16_nn: A=[M,K], B=[K,N] → B=W[eid] which is already [in, out] = [K, N] ✓
"""
eid = self._embodiment_id
logger.info("Extracting embodiment weights for '%s' (id=%d)", self._embodiment_tag, eid)
# State encoder: layer1 [128→1024] + ReLU + layer2 [1024→1536]
# W[eid] is already [in, out] = [K, N] for fp16_nn — NO transpose! (C13)
# ★ All embodiment weights → fp16 ★
self._state_enc_w1 = sd[f"{AH_PREFIX}.state_encoder.layer1.W"][eid].contiguous().to(fp16) # [128, 1024]
self._state_enc_b1 = sd[f"{AH_PREFIX}.state_encoder.layer1.b"][eid].to(fp16) # [1024]
self._state_enc_w2 = sd[f"{AH_PREFIX}.state_encoder.layer2.W"][eid].contiguous().to(fp16) # [1024, 1536]
self._state_enc_b2 = sd[f"{AH_PREFIX}.state_encoder.layer2.b"][eid].to(fp16) # [1536]
# Action encoder: W1 [128→1536], W2 [3072→1536] (concat action+time), W3 [1536→1536]
self._action_enc_w1 = sd[f"{AH_PREFIX}.action_encoder.W1.W"][eid].contiguous().to(fp16)
self._action_enc_b1 = sd[f"{AH_PREFIX}.action_encoder.W1.b"][eid].to(fp16)
self._action_enc_w2 = sd[f"{AH_PREFIX}.action_encoder.W2.W"][eid].contiguous().to(fp16)
self._action_enc_b2 = sd[f"{AH_PREFIX}.action_encoder.W2.b"][eid].to(fp16)
self._action_enc_w3 = sd[f"{AH_PREFIX}.action_encoder.W3.W"][eid].contiguous().to(fp16)
self._action_enc_b3 = sd[f"{AH_PREFIX}.action_encoder.W3.b"][eid].to(fp16)
# Action decoder: layer1 [1024→1024] + ReLU + layer2 [1024→128]
self._action_dec_w1 = sd[f"{AH_PREFIX}.action_decoder.layer1.W"][eid].contiguous().to(fp16)
self._action_dec_b1 = sd[f"{AH_PREFIX}.action_decoder.layer1.b"][eid].to(fp16)
self._action_dec_w2 = sd[f"{AH_PREFIX}.action_decoder.layer2.W"][eid].contiguous().to(fp16)
self._action_dec_b2 = sd[f"{AH_PREFIX}.action_decoder.layer2.b"][eid].to(fp16)
# No mask_token — state_dropout_prob=0.0 (C14)
logger.info("Embodiment weights extracted: state[128→1024→1536], "
"action_enc[128→1536, 3072→1536, 1536→1536], "
"action_dec[1024→1024→128]")
# ─────────────────────────────────────────────────────────────
# Precompute
# ─────────────────────────────────────────────────────────────
def _precompute_rope(self):
"""Precompute Qwen3 RoPE weights (theta=1e6, HD=128)."""
theta = 1000000.0
max_seq = 2048
HD = self.HD_llm
freqs = 1.0 / (theta ** (torch.arange(0, HD, 2, dtype=torch.float32, device='cuda') / HD))
positions = torch.arange(max_seq, dtype=torch.float32, device='cuda')
angles = torch.outer(positions, freqs) # [max_seq, HD//2]
# For Qwen3 rotate_half RoPE: cos/sin broadcasted to full HD
self._rope_cos_cache = torch.cat([torch.cos(angles), torch.cos(angles)], dim=-1).to(fp16) # [max_seq, HD]
self._rope_sin_cache = torch.cat([torch.sin(angles), torch.sin(angles)], dim=-1).to(fp16)
# Legacy format for csrc rope_apply (pair-interleaved)
self._rope_weights = torch.cat([
torch.cos(angles), torch.sin(angles)
], dim=-1).to(fp16)
logger.info("RoPE precomputed: theta=%.0e, max_seq=%d, HD=%d", theta, max_seq, HD)
def _precompute_timesteps(self):
"""Precompute timestep embeddings for 4 flow-matching steps.
For each step t ∈ {0, 1, 2, 3}:
t_cont = t / 4.0 → {0.0, 0.25, 0.5, 0.75}
t_disc = int(t_cont * 1000) → {0, 250, 500, 750}
Pipeline:
t_disc → Timesteps(256, flip_sin_to_cos=True, downscale_freq_shift=1)
→ sinusoidal [256]
→ linear_1 [256→1536] → SiLU → linear_2 [1536→1536]
→ temb [1536]
Per-block AdaLN conditioning (precomputed for all 32 layers × 4 steps):
SiLU(temb) → norm1.linear [1536→3072] → split → (shift, scale) (C11: shift first!)
Final output conditioning (precomputed for 4 steps):
SiLU(temb) → proj_out_1 [1536→3072] → split → (shift, scale)
"""
with torch.no_grad():
D = self.D_dit # 1536
# Step 1: Sinusoidal time encoding (matches diffusers Timesteps)
# flip_sin_to_cos=True, downscale_freq_shift=1
half_dim = 128 # 256 / 2
exponent = -torch.arange(half_dim, dtype=torch.float32, device='cuda') * \
(math.log(10000.0) / half_dim)
emb_freqs = exponent.exp() # [128]
t_values = [0, 250, 500, 750]
self._tembs = [] # [4, D] — one temb per step
self._ada_scales = [] # [4, L, D] — per-step per-layer scale
self._ada_shifts = [] # [4, L, D]
self._out_scales = [] # [4, D]
self._out_shifts = [] # [4, D]
for t_disc in t_values:
# Sinusoidal encoding (flip_sin_to_cos=True, downscale_freq_shift=1)
t_tensor = torch.tensor([t_disc], dtype=torch.float32, device='cuda')
args = t_tensor[:, None] * emb_freqs[None, :] # [1, 128]
sincos = torch.cat([torch.cos(args), torch.sin(args)], dim=-1) # [1, 256]
# TimestepEmbedding: linear_1 → SiLU → linear_2
temb = sincos.to(fp16) @ self._ts_linear1_w + self._ts_linear1_b # [1, 1536]
temb = F.silu(temb)
temb = temb @ self._ts_linear2_w + self._ts_linear2_b # [1, 1536]
self._tembs.append(temb.squeeze(0)) # [1536]
# Per-layer AdaLN: SiLU(temb) → linear → split(scale, shift)
# ★ AdaLayerNorm: scale FIRST, shift SECOND (line 80 in dit.py)
# ★ Output conditioning: shift FIRST, scale SECOND (line 394)
silu_temb = F.silu(temb) # [1, 1536]
layer_scales = []
layer_shifts = []
for l in range(self.L_dit):
ada_out = silu_temb @ self._dit_w['norm1_linear_w'][l] # [1, 3072]
ada_out = ada_out + self._dit_w['norm1_linear_b'][l]
scale, shift = ada_out.squeeze(0).chunk(2, dim=0) # ★ scale first! ★
layer_scales.append(scale)
layer_shifts.append(shift)
self._ada_scales.append(torch.stack(layer_scales)) # [L, D]
self._ada_shifts.append(torch.stack(layer_shifts)) # [L, D]
# Final output conditioning: SiLU(temb) → proj_out_1 → split(shift, scale)
out_cond = silu_temb @ self._dit_proj_out_1_w + self._dit_proj_out_1_b # [1, 3072]
out_shift, out_scale = out_cond.squeeze(0).chunk(2, dim=0) # ★ shift first ★
self._out_scales.append(out_scale)
self._out_shifts.append(out_shift)
# Stack for efficient indexing
self._tembs = torch.stack(self._tembs) # [4, D]
self._ada_scales = torch.stack(self._ada_scales) # [4, L, D]
self._ada_shifts = torch.stack(self._ada_shifts) # [4, L, D]
self._out_scales = torch.stack(self._out_scales) # [4, D]
self._out_shifts = torch.stack(self._out_shifts) # [4, D]
logger.info("Timestep embeddings precomputed: 4 steps × %d layers, temb[%s]",
self.L_dit, list(self._tembs.shape))
def _allocate_buffers(self):
"""Allocate GPU buffers for pipeline."""
nv = self._num_views
S_sig = nv * self.spv_raw # 2 * 256 = 512 (raw SigLIP patches)
S_img = nv * self.spv # 2 * 64 = 128 (after pixel unshuffle)
D_sig = self.D_sig # 1152
D_llm = self.D_llm # 2048
D_dit = self.D_dit # 1536
H_sig = self.H_sig # 4304
Sa = self.Sa # 51
# ── SigLIP2 buffers ──
self._sig_x = torch.zeros(S_sig, D_sig, dtype=fp16, device='cuda')
self._sig_x_fp8 = torch.zeros(S_sig * D_sig, dtype=torch.uint8, device='cuda')
self._sig_qkv = torch.empty(S_sig, 3 * D_sig, dtype=fp16, device='cuda')
self._sig_attn = torch.empty(S_sig, D_sig, dtype=fp16, device='cuda')
self._sig_hidden = torch.empty(S_sig, H_sig, dtype=fp16, device='cuda')
self._sig_hid_fp8 = torch.zeros(S_sig * H_sig, dtype=torch.uint8, device='cuda')
# FP16-path scratch (x_norm / fg) required by ``_siglip_forward_fp16``;
# harmless extra buffers when running the FP8 path.
self._sig_x_norm = torch.empty(S_sig, D_sig, dtype=fp16, device='cuda')
self._sig_fg = torch.empty(S_sig, D_sig, dtype=fp16, device='cuda')
self._sig_bufs = {
'x': self._sig_x.data_ptr(),
'x_fp8': self._sig_x_fp8.data_ptr(),
'qkv': self._sig_qkv.data_ptr(),
'attn_out': self._sig_attn.data_ptr(),
'hidden': self._sig_hidden.data_ptr(),
'hid_fp8': self._sig_hid_fp8.data_ptr(),
'x_norm': self._sig_x_norm.data_ptr(),
'fg': self._sig_fg.data_ptr(),
}
# ── mlp1 buffer (after pixel unshuffle) ──
self._mlp1_in = torch.empty(S_img, self.mlp1_in, dtype=fp16, device='cuda') # [128, 4608]
self._mlp1_mid = torch.empty(S_img, D_llm, dtype=fp16, device='cuda') # [128, 2048]
self._vision_features = torch.empty(S_img, D_llm, dtype=fp16, device='cuda') # [128, 2048]
# ── Qwen3/backbone placeholder (Se depends on prompt_len) ──
# Allocated dynamically in set_prompt()
# DiT/Qwen3 buffers managed by CKernel classes (created in _capture_all_graphs)
logger.info("SigLIP buffers allocated. S_sig=%d, S_img=%d", S_sig, S_img)
# ─────────────────────────────────────────────────────────────
# Kernel helpers (validated E2E cos=0.999975)
# ─────────────────────────────────────────────────────────────
def _fp16_gemm(self, A, B, M, N, K):
"""fp16_nn wrapper: C[M,N] = A[M,K] @ B[K,N]. All fp16."""
C = torch.empty(M, N, dtype=fp16, device='cuda')
self._gemm.fp16_nn(A.data_ptr(), B.data_ptr(), C.data_ptr(), M, N, K, 0)
return C
def _fp8_gemm(self, A_fp16, B_fp8, w_scale_ptr, M, N, K):
"""FP8 GEMM with pre-quantized weight + pre-allocated scale ptr.
Quantizes activation on-the-fly.
"""
A_fp8 = self._fp8_act_buf # pre-allocated in _allocate_buffers
fvk.quantize_fp8_static_fp16(
A_fp16.data_ptr(), A_fp8.data_ptr(),
self._unit_scale.data_ptr(), M * K, 0)
C = torch.empty(M, N, dtype=fp16, device='cuda')
self._gemm.fp8_descale_fp16(
A_fp8.data_ptr(), B_fp8.data_ptr(), C.data_ptr(),
M, N, K,
self._unit_scale.data_ptr(), w_scale_ptr, 0)
return C
def _sinusoidal_time_embed(self, timesteps, dim=1536):
"""Sinusoidal positional encoding for action encoder."""
half_dim = dim // 2
exp = -torch.arange(half_dim, dtype=torch.float, device='cuda') * \
(math.log(10000.0) / half_dim)
freqs = timesteps.unsqueeze(-1).float() * exp.exp()
return torch.cat([torch.sin(freqs), torch.cos(freqs)], dim=-1)
def _timestep_encode(self, t_disc):
"""Timesteps(256) → TimestepEmbedding → temb [1, 1536]."""
half_dim = 128
exp = -torch.arange(half_dim, dtype=torch.float32, device='cuda') * \
(math.log(10000.0) / half_dim)
t_tensor = torch.tensor([t_disc], dtype=torch.float32, device='cuda')
args = t_tensor[:, None] * exp.exp()
sincos = torch.cat([torch.cos(args), torch.sin(args)], dim=-1).to(fp16)
temb = F.silu(sincos @ self._ts_linear1_w + self._ts_linear1_b)
temb = temb @ self._ts_linear2_w + self._ts_linear2_b
return temb # [1, 1536]
def _state_encode(self, state):
"""[1, state_dim] → [1, 1, D_dit]. Embodiment-specific MLP."""
state = state.to(fp16).contiguous()
if state.dim() == 1:
state = state.unsqueeze(0)
h = F.relu(self._fp16_gemm(state, self._state_enc_w1, 1, 1024, self.state_dim)
+ self._state_enc_b1)
h = self._fp16_gemm(h, self._state_enc_w2, 1, self.D_dit, 1024) + self._state_enc_b2
return h.unsqueeze(0) # [1, 1, D_dit]
def _copy_state_feature_to_dit(self, state):
"""Encode current robot state into the captured DiT input buffer.
Uses the DiT head's own state encoder (``self._g_dit.se_w1/se_w2``) —
the same path the capture-time encode in ``_capture_all_graphs`` takes.
(``self._state_encode`` relies on ``_state_enc_w1``, which is only
populated by the unused single-graph ``_load_embodiment_weights`` path
and is absent on this g_dit pipeline.)
"""
if isinstance(state, np.ndarray):
state = torch.from_numpy(state).to(torch.float32).cuda()
elif not isinstance(state, torch.Tensor):
state = torch.as_tensor(state, dtype=torch.float32, device='cuda')
state_fp16 = state.to(device='cuda', dtype=fp16).contiguous()
if state_fp16.dim() == 1:
state_fp16 = state_fp16.unsqueeze(0)
h = torch.empty(1, 1024, dtype=fp16, device='cuda')
self._g_dit.gemm.fp16_nn(state_fp16.data_ptr(), self._g_dit.se_w1.data_ptr(),
h.data_ptr(), 1, 1024, self.state_dim, 0)
fvk.add_bias_fp16(h.data_ptr(), self._g_dit.se_b1.data_ptr(), 1, 1024, 0)
fvk.relu_inplace_fp16(h.data_ptr(), 1024, 0)
sf = torch.empty(1, self.D_dit, dtype=fp16, device='cuda')
self._g_dit.gemm.fp16_nn(h.data_ptr(), self._g_dit.se_w2.data_ptr(),
sf.data_ptr(), 1, self.D_dit, 1024, 0)
fvk.add_bias_fp16(sf.data_ptr(), self._g_dit.se_b2.data_ptr(), 1, self.D_dit, 0)
self._g_dit.b_state_feat.copy_(sf)
def _action_encode(self, actions, t_disc, action_horizon):
"""[1, T, action_dim] + timestep → [1, T, D_dit]."""
T = action_horizon
D = self.D_dit
actions_2d = actions.squeeze(0).to(fp16).contiguous()
a_emb = self._fp16_gemm(actions_2d, self._action_enc_w1, T, D, self.action_dim) \
+ self._action_enc_b1
t_expanded = torch.full((T,), t_disc, device='cuda')
time_emb = self._sinusoidal_time_embed(t_expanded, D).to(fp16)
concat = torch.cat([a_emb, time_emb], dim=-1) # [T, 2*D]
h = F.silu(self._fp16_gemm(concat, self._action_enc_w2, T, D, 2 * D)
+ self._action_enc_b2)
h = self._fp16_gemm(h, self._action_enc_w3, T, D, D) + self._action_enc_b3
pos_ids = torch.arange(T, device='cuda')
h = h + self._position_embedding[pos_ids]
return h.unsqueeze(0) # [1, T, D]
def _action_decode(self, model_output, action_horizon):
"""[1, Sa, output_dim] → velocity [1, T, action_dim]."""
Sa = model_output.shape[1]
x = model_output.squeeze(0).to(fp16)
h = F.relu(self._fp16_gemm(x, self._action_dec_w1, Sa, 1024, self.output_dim)
+ self._action_dec_b1)
pred = self._fp16_gemm(h, self._action_dec_w2, Sa, self.action_dim, 1024) \
+ self._action_dec_b2
return pred[-action_horizon:].unsqueeze(0)
def _dit_layer(self, hidden, l, temb, is_self, backbone=None, attn_mask=None):
"""Single DiT layer: AdaLN → Attention → Residual → LN → GELU FFN → Residual.
Uses FP8 GEMM for large matrix ops (QKV merged, FFN up/down, O proj).
Small ops (bias, LN, GELU, SDPA) remain fp16.
"""
D = self.D_dit
NH, HD = self.NH_dit, self.HD_dit
H = self.H_dit
B, S_q = hidden.shape[0], hidden.shape[1]
w = self._dit_w
# AdaLayerNorm: LN(x, no params) * (1+scale) + shift
ada_out = F.silu(temb) @ w['norm1_linear_w'][l] + w['norm1_linear_b'][l]
scale, shift = ada_out.chunk(2, dim=-1) # scale first (AdaLayerNorm)
h_norm = F.layer_norm(hidden.float(), [D], eps=1e-5).to(fp16)
h_norm = h_norm * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
# Attention — FP8 for large GEMMs (QKV merged, FFN), fp16 for small (Q/K/V cross, O)
h_norm_2d = h_norm.squeeze(0).contiguous()
if is_self:
# Self-attn: merged QKV is large (S×4608) → FP8
qkv = self._fp8_gemm(h_norm_2d, w['qkv_w_fp8'][l], w['qkv_s'][l].data_ptr(), S_q, 3*D, D)
qkv = qkv + w['qkv_b'][l]
q = qkv[:, :D].view(S_q, NH, HD).unsqueeze(0).transpose(1, 2)
k = qkv[:, D:2*D].view(S_q, NH, HD).unsqueeze(0).transpose(1, 2)
v = qkv[:, 2*D:].view(S_q, NH, HD).unsqueeze(0).transpose(1, 2)
attn = F.scaled_dot_product_attention(q, k, v)
else:
# Cross-attn: Q/K/V separate, small M → keep fp16
q = self._fp16_gemm(h_norm_2d, w['q_w'][l], S_q, D, D) + w['q_b'][l]
kv_src = backbone.to(fp16)
if attn_mask is not None:
kv_src = kv_src * attn_mask.unsqueeze(-1).to(fp16)
S_kv, D_kv = kv_src.shape[1], kv_src.shape[2]
kv_2d = kv_src.squeeze(0).contiguous()
k = self._fp16_gemm(kv_2d, w['k_w'][l], S_kv, D, D_kv) + w['k_b'][l]
v = self._fp16_gemm(kv_2d, w['v_w'][l], S_kv, D, D_kv) + w['v_b'][l]
q = q.view(S_q, NH, HD).unsqueeze(0).transpose(1, 2)
k = k.view(S_kv, NH, HD).unsqueeze(0).transpose(1, 2)
v = v.view(S_kv, NH, HD).unsqueeze(0).transpose(1, 2)
amask = None
if attn_mask is not None:
amask = attn_mask.unsqueeze(1).unsqueeze(2).expand(B, NH, S_q, S_kv)
amask = torch.where(amask, 0.0, float('-inf')).to(fp16)
attn = F.scaled_dot_product_attention(q, k, v, attn_mask=amask)
attn = attn.transpose(1, 2).reshape(B, S_q, D).to(fp16)
attn_2d = attn.squeeze(0).contiguous()
o = self._fp16_gemm(attn_2d, w['o_w'][l], S_q, D, D) + w['o_b'][l]
hidden = hidden.float() + o.unsqueeze(0).float()
# FFN: LN(no params) → GELU → down — FP8 for large FFN GEMMs
ff_norm = F.layer_norm(hidden, [D], eps=1e-5).to(fp16)
ff_norm_2d = ff_norm.squeeze(0).contiguous()
ff_h = self._fp8_gemm(ff_norm_2d, w['ff_up_w_fp8'][l], w['ff_up_s'][l].data_ptr(), S_q, H, D)
ff_h = ff_h + w['ff_up_b'][l]
fvk.gelu_inplace_fp16(ff_h.data_ptr(), S_q * H, 0)
ff_out = self._fp8_gemm(ff_h, w['ff_down_w_fp8'][l], w['ff_down_s'][l].data_ptr(), S_q, D, H)
ff_out = ff_out + w['ff_down_b'][l]
hidden = (hidden + ff_out.unsqueeze(0).float()).to(fp16)
return hidden
def _dit_forward(self, sa_embs, backbone_features, image_mask, backbone_mask, temb):
"""Full 32-layer DiT forward for a single flow-matching step."""
D = self.D_dit
non_image_mask = (~image_mask) & backbone_mask
image_attn_mask = image_mask & backbone_mask
hidden = sa_embs
for l in range(self.L_dit):
is_self = (l % 2 == 1)
if is_self:
hidden = self._dit_layer(hidden, l, temb, True)
else:
curr_mask = non_image_mask if l % 4 == 0 else image_attn_mask
hidden = self._dit_layer(hidden, l, temb, False,
backbone_features, curr_mask)
# Output conditioning: shift first, scale second
out_cond = F.silu(temb) @ self._dit_proj_out_1_w + self._dit_proj_out_1_b
out_shift, out_scale = out_cond.chunk(2, dim=-1)
hidden = F.layer_norm(hidden.float(), [D], eps=1e-6).to(fp16)
hidden = hidden * (1 + out_scale.unsqueeze(1)) + out_shift.unsqueeze(1)
Sa = hidden.shape[1]
output = self._fp16_gemm(hidden.squeeze(0), self._dit_proj_out_2_w, Sa, self.output_dim, D)
return output.unsqueeze(0) # [1, Sa, output_dim]
# ─────────────────────────────────────────────────────────────
# Public API
# ─────────────────────────────────────────────────────────────
def set_prompt(self, prompt):
"""Tokenize prompt and prepare text embeddings for Qwen3 backbone."""
if getattr(self, '_graphs_built', False):
raise RuntimeError(
"set_prompt() after the pipeline is built is not supported; "
"construct a new GrootTorchFrontendThor instance for a new prompt")
from transformers import AutoTokenizer
if not hasattr(self, '_tokenizer'):
eagle_dir = pathlib.Path(__file__).parent.parent.parent.parent / "configs"
# Try multiple tokenizer locations
for tok_path in [
str(self._checkpoint_path), # checkpoint dir may have tokenizer
str(self._checkpoint_path / "tokenizer"), # subfolder
# Local GROOT code Eagle dir
str(pathlib.Path(__file__).parent.parent.parent.parent.parent /
"GR00T" / "Isaac-GR00T" / "gr00t" / "model" / "modules" / "nvidia" / "Eagle-Block2A-2B-v2"),
"nvidia/Eagle-Block2A-2B-v2", # HF hub (fallback)
]:
try:
self._tokenizer = AutoTokenizer.from_pretrained(tok_path, trust_remote_code=True)
break
except Exception:
continue
if not hasattr(self, '_tokenizer'):
raise RuntimeError("Cannot load Qwen3 tokenizer")
self._img_token_id = 151669 # <IMG_CONTEXT>
self._img_start_id = 151670 # <img>
self._img_end_id = 151671 # </img>
S_img = self._num_views * self.spv # image tokens after pixel unshuffle
text_ids = self._tokenizer.encode(prompt, add_special_tokens=False)
# Build: text + <img> + <IMG_CONTEXT>*S_img + </img>
full_ids = text_ids + [self._img_start_id] + [self._img_token_id] * S_img + [self._img_end_id]
self._input_ids = torch.tensor([full_ids], dtype=torch.long, device='cuda')
self._text_len = len(text_ids)
self._Se = len(full_ids)
self._prompt_text = prompt
# Pre-compute text embeddings (text portion only; image tokens replaced at infer time)
self._text_embeds = F.embedding(self._input_ids, self._qwen3_embed) # [1, Se, 2048]
# Masks
self._image_mask = (self._input_ids == self._img_token_id) # [1, Se]
self._backbone_mask = torch.ones(1, self._Se, dtype=torch.bool, device='cuda')
logger.info("Prompt set: '%s' (%d text + %d img = %d total tokens)",
prompt[:50], self._text_len, S_img, self._Se)
def infer_action_head(self, backbone_features, image_mask, backbone_mask,
state, action_horizon=None, noise_seed=None):
"""Run GROOT action head inference (DiT + embodiment MLPs).
Assumes backbone features are already computed (SigLIP2 + Qwen3).
This is the validated E2E path (cos=0.999975 vs PyTorch reference).
Args:
backbone_features: [1, Se, 2048] — output of vlln(Qwen3(SigLIP2+text))
image_mask: [1, Se] boolean — True for image tokens
backbone_mask: [1, Se] boolean — True for valid tokens
state: [state_dim] or [1, state_dim] tensor
action_horizon: number of action steps (default: self.action_horizon)
noise_seed: random seed for initial noise (default: None = random)
Returns:
actions: [1, action_horizon, action_dim] tensor
"""
if action_horizon is None:
action_horizon = self.action_horizon
B = 1
D = self.D_dit
# State encoding
state_feat = self._state_encode(state) # [1, 1, D_dit]
# Init noise
if noise_seed is not None:
torch.manual_seed(noise_seed)
actions = torch.randn(B, action_horizon, self.action_dim,
dtype=torch.float32, device='cuda')
dt = 1.0 / self.num_steps
# 4-step flow matching
for step in range(self.num_steps):
t_cont = step / float(self.num_steps)
t_disc = int(t_cont * 1000)
temb = self._timestep_encode(t_disc)
action_feat = self._action_encode(actions, t_disc, action_horizon)
sa_embs = torch.cat([state_feat, action_feat], dim=1) # [1, 1+T, D_dit]
model_output = self._dit_forward(
sa_embs, backbone_features, image_mask, backbone_mask, temb)
velocity = self._action_decode(model_output, action_horizon)
actions = actions + dt * velocity.float()