File size: 88,470 Bytes
90ca518
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
"""fix3 生成器模型(已融合: 基类 + alt-PC 双云注入 + TSSR 三前向)。

类继承链(融合后仍保留, 因各层职责清晰):
    FactorizedDiffusionTransformer      骨干: 因子分解 token / AdaLN cross-attn / 自回归行头
      └ FactorizedDiffusionTransformerAltPC   逐层交替双点云注入 + 几何 RoPE + sample()
          └ FactorizedTSSRTransformer         mode flag + 逐 token 正确性分类器 + sample_tssr()

原先的 FactorizedDiffusionTransformerFocal 一层只重写训练用的 factorized_loss, 推理路径上是空壳,
已移除并让 AltPC 直接继承骨干。各类的纯训练方法(factorized_loss / factorized_accuracy /
sample_log / forward_mask / forward_nomask / zero_touch_conditional_params)也已剥离。
"""

import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from models.subdivider import (
    SinusoidalPosEmb,
    ModernCrossAttnTransformerBlock,
    ModernAttention,
    RMSNorm,
    SwiGLU,
    RotaryEmbedding,
    LinearDownsample,
    LinearUpsample,
    apply_rotary_pos_emb,
    apply_rotary_pos_emb_varlen,
    lengths_to_position_ids,
    cosine_schedule,
)
from models.point_conditioner import PointConditioner
import os
import numpy as np
from models.subdivider import (
    apply_rotary_pos_emb_varlen, lengths_to_position_ids, cosine_schedule,
)


# ==========================================================================
# 原 model_acc_fix3.py
# ==========================================================================

def modulate(x, shift, scale):
    """adaLN 调制: x * (1 + scale) + shift。shift/scale 与 x 同形状(逐 token 广播)。"""
    return x * (1 + scale) + shift
class AdaLNCrossAttnBlock(nn.Module):
    """fix3 专用 block:在 self-attn 与 FFN 上加 adaLN 调制(条件 c = time + depth)。
    cross-attn 仍走点云条件(不调制)。结构与 ModernCrossAttnTransformerBlock 同名(self_attn/
    cross_attn/norm1-3/ffn)以便迁移 fix2 权重;adaLN_modulation 为**新增**且做「恒等初始化」
    (shift=0, scale=0, gate=1),使加载迁移权重时 block == 旧 block,backbone 不被破坏。"""

    def __init__(self, d_model, nhead, dropout=0., qk_norm=False, ffn_hidden=None):
        super().__init__()
        # QK-norm 只加在 self-attn(instability 主源);cross-attn 的 K 是点云条件,保持默认不动
        self.self_attn = ModernAttention(d_model, nhead, dropout=dropout, qk_norm=qk_norm)
        self.cross_attn = ModernAttention(d_model, nhead, dropout=dropout)
        self.norm1 = RMSNorm(d_model)
        self.norm2 = RMSNorm(d_model)
        self.norm3 = RMSNorm(d_model)
        self.ffn = SwiGLU(d_model, ffn_hidden)
        self.dropout = nn.Dropout(dropout)
        # 从条件 c 产生 self-attn / FFN 各自的 (shift, scale, gate),共 6*d_model
        self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(d_model, 6 * d_model))
        # 恒等初始化:weight=0;bias 使 shift=0, scale=0, gate=1 -> block 退化为旧 block
        nn.init.zeros_(self.adaLN_modulation[1].weight)
        with torch.no_grad():
            b = torch.zeros(6 * d_model)
            b[2 * d_model:3 * d_model] = 1.0   # gate_sa = 1
            b[5 * d_model:6 * d_model] = 1.0   # gate_ff = 1
            self.adaLN_modulation[1].bias.copy_(b)

    def forward_varlen(self, x, c, lengths, cond=None, cond_lengths=None, rope_apply_fn=None,
                       max_len=None, cond_max_len=None):
        """c: [total, d_model] 逐 token 条件(time+depth)。
        max_len / cond_max_len: 透传给 attention 的 max_seqlen(python int), 省掉 GPU->CPU 同步;
        不传则由 attention 内部回退到 `.item()`(见 ModernAttention.forward_varlen 的说明)。

        注: 曾试过「c 只传 [b,d]、在块内 adaLN 后再 repeat_interleave」以省掉 768->6*768 Linear
        在同一行上重复算 total 次的冗余(每块 424 GFLOP@60k token) —— 实测输出逐位相同但**零加速**
        (hourglass 已把 token 降到 1/3、1/9, 那 3.1 TFLOP 不是瓶颈), 且会让 torch.compile 报
        dtype 不匹配。故回退, 别再试。
        """
        # c 来自 time_mlp + depth_embedding, 是 fp32(nn.Embedding 不在 autocast 转型名单里)。
        # 对齐到 x 的 dtype: eager+autocast 下等价(Linear 本来就会转), 但 torch.compile 编译这个方法时
        # 会丢掉外层 autocast 上下文, 不显式对齐就会在后面的 matmul 上报 "Float and BFloat16"。
        c = c.to(x.dtype)
        shift_sa, scale_sa, gate_sa, shift_ff, scale_ff, gate_ff = self.adaLN_modulation(c).chunk(6, dim=-1)
        h = modulate(self.norm1(x), shift_sa, scale_sa)
        sa = self.self_attn.forward_varlen(h, h, h, lengths, rope_apply_fn=rope_apply_fn,
                                           max_len_q=max_len)
        x = x + gate_sa * self.dropout(sa)
        if cond is not None:
            h = self.norm2(x)
            # CFG drop 的步里 cond 来自 fp32 的 null_cond; 同 c 一样显式对齐(见上), 否则 compile 会崩
            ca = self.cross_attn.forward_varlen(h, cond.to(x.dtype), cond.to(x.dtype), lengths,
                                                lengths_k=cond_lengths,
                                                max_len_q=max_len, max_len_k=cond_max_len)
            x = x + self.dropout(ca)
        h = modulate(self.norm3(x), shift_ff, scale_ff)
        ff = self.ffn(h)
        x = x + gate_ff * self.dropout(ff)
        return x
SUB_VOCAB = 256          # 每个子码(8bit)的取值数
MASK_ID = SUB_VOCAB      # 子码 MASK id = 256(embedding 表大小 = 257)
NUM_OCTANTS = 8
TYPE_F, TYPE_INTRA, TYPE_INTER = 0, 1, 2
REFINE_POS_PER_FACE = 9  # 3 F + 3 intra + 3 inter
class AutoregressiveRowHead(nn.Module):
    """连接矩阵(intra/inter)的 8 行**自回归** MLP 头(参考 TreeMeshGPT 的 head_coord1/2/3)。

    取代原先「8 行并行独立预测」:行 r 的 256-way logits 由
        cat([hidden, emb(行0), ..., emb(行r-1)])  -> 3 层 MLP
    给出,从而**显式建模行间依赖**,让模型只产出能共现的合法矩阵(对称/可成 clique),
    减少独立预测导致的「悬边→退化面/碎块」。
      * 训练: teacher forcing(回喂 GT 行 embedding),一次 forward 出全 8 行 logits。
      * 采样: 逐行 argmax rollout(回喂已采行);支持 CFG —— cond/uncond 在每行 logits 级合并,
              合并后采样得到的同一行回喂两路,保持自回归状态一致。
    行 embedding 维度默认 = d_model//8(与输入端 row_embed_dim 一致,控参数量);最后一行不作输入故只需 7 张表。
    """

    def __init__(self, d_model, num_rows=NUM_OCTANTS, vocab=SUB_VOCAB, row_emb_dim=None):
        super().__init__()
        self.num_rows = num_rows
        self.vocab = vocab
        self.row_emb_dim = row_emb_dim or (d_model // num_rows)
        self.mlps = nn.ModuleList([
            nn.Sequential(
                nn.Linear(d_model + r * self.row_emb_dim, d_model), nn.ReLU(),
                nn.Linear(d_model, d_model), nn.ReLU(),
                nn.Linear(d_model, vocab),
            ) for r in range(num_rows)
        ])
        self.row_emb = nn.ModuleList([nn.Embedding(vocab, self.row_emb_dim) for _ in range(num_rows - 1)])

    def forward_teacher(self, h, gt_rows):
        """h [n,d];gt_rows [n,num_rows] long(取值[0,vocab)) -> logits [n,num_rows,vocab]。teacher forcing。"""
        feats = [h]
        logits = []
        for r in range(self.num_rows):
            logits.append(self.mlps[r](torch.cat(feats, dim=-1)))
            if r < self.num_rows - 1:
                feats.append(self.row_emb[r](gt_rows[:, r]).to(h.dtype))
        return torch.stack(logits, dim=1)

    def rollout(self, h, h_u=None, cfg_scale=1.0, temperature=0.0):
        """逐行采样。h [n,d];h_u 提供则做 CFG(logits=uncond+w*(cond-uncond))。
        temperature<=0: argmax 贪心(确定性);>0: 按 (logit/temperature) 多项式采样(用于 best-of-N 产生多样候选)。
        返回 (pred [n,num_rows] long, conf [n,num_rows] 每行所选类的概率)。"""
        feats = [h]
        feats_u = [h_u] if h_u is not None else None
        preds, confs = [], []
        for r in range(self.num_rows):
            logit = self.mlps[r](torch.cat(feats, dim=-1))
            if feats_u is not None:
                lu = self.mlps[r](torch.cat(feats_u, dim=-1))
                logit = lu + cfg_scale * (logit - lu)
            if temperature and temperature > 0:
                prob = (logit / temperature).softmax(-1)
                am = torch.multinomial(prob.float(), 1).squeeze(-1)              # [n]
                pm = prob.gather(-1, am.unsqueeze(-1)).squeeze(-1)               # 所选类概率
            else:
                prob = logit.softmax(-1)
                pm, am = prob.max(-1)
            preds.append(am); confs.append(pm)
            if r < self.num_rows - 1:
                e = self.row_emb[r](am).to(h.dtype)
                feats.append(e)
                if feats_u is not None:
                    feats_u.append(e)
        return torch.stack(preds, dim=1), torch.stack(confs, dim=1)
class FactorizedDiffusionTransformer(nn.Module):
    def __init__(self,
                 coord_vocab_size,         # raw_embedding 大小 = resolution+1 (coord 含 offset, 0=pad)
                 max_seq_len,
                 d_model=768,
                 nhead=12,
                 depth=(4, 4, 8),
                 dropout=0.,
                 encoder_freeze=True,
                 encoder_name='miche-256-feature',
                 use_pc=True,
                 time_embed_scale=100.0,
                 num_depths=16,            # depth(层级) embedding 大小,覆盖 target_depth 0..max
                 nonempty_row_weight=1.0,  # 连接矩阵「非空行」(目标!=0, 即真实连接) 的 loss 上权
                 qk_norm=False,            # self-attn 的 Q/K 加 per-head RMSNorm(稳定 attention);默认关
                 ffn_hidden=None,          # FFN 隐层维度显式覆盖(None=8/3*d_model; 1024宽用2816对齐TensorCore)
                 pc_num_latents=None,      # 点云编码器 latent 数(None=预训练默认256; 1024=条件带宽x4, query平铺扩容)
                 time_embed_dim=None):     # 正弦基底/time_mlp 隐层维度(None=d_model, 默认即可)。备注:
                                           # SinusoidalPosEmb 的"列-频率/sin-cos"布局随 dim 变化, 宽度扩展
                                           # 热启动时旧 time_mlp 权重不可拷贝(语义错位会爆), 见扩展脚本。
        super().__init__()
        self.d_model = d_model
        self.mask_token_id = MASK_ID
        self.num_depths = num_depths
        # 连接(intra/inter)行子码中「非空行」(target!=0) 的 loss 权重。connectivity 矩阵 ~92% 是空行,
        # 等权 CE 会让模型坍缩到「全预测空行」(逐子码 acc 虚高、生成 mesh 欠连接碎裂)。>1 上权真实连接行。
        self.nonempty_row_weight = float(nonempty_row_weight)

        # 1. Embeddings ----------------------------------------------------
        self.raw_embedding = nn.Embedding(coord_vocab_size, d_model)        # coord(条件)
        self.f_sub_embedding = nn.Embedding(SUB_VOCAB + 1, d_model)         # F: 单子码
        # intra/inter: 8 个行子码各查表(+1 行给 MASK),再 cat 后 Linear 投影到 d_model。
        # 用 cat 而非 sum:保留 8 行的「行序/位置语义」(行 r = octant r),避免 sum 的置换不变与叠加丢失;
        # 行 embedding 维度 = d_model // 8,cat 后恰好 = d_model,再过一层 Linear 学行间组合(含 intra 对称)。
        self.row_embed_dim = d_model // NUM_OCTANTS
        self.intra_sub_embedding = nn.Embedding(SUB_VOCAB + 1, self.row_embed_dim)
        self.inter_sub_embedding = nn.Embedding(SUB_VOCAB + 1, self.row_embed_dim)
        self.intra_in_proj = nn.Linear(NUM_OCTANTS * self.row_embed_dim, d_model)
        self.inter_in_proj = nn.Linear(NUM_OCTANTS * self.row_embed_dim, d_model)

        # 2. 条件: 时间步 + 层级(depth) —— 一起作为 adaLN 的条件 c -------------
        te = int(time_embed_dim) if time_embed_dim else d_model
        self.time_mlp = nn.Sequential(
            SinusoidalPosEmb(te, scale=time_embed_scale),
            nn.Linear(te, te * 4),
            nn.GELU(),
            nn.Linear(te * 4, d_model),
        )
        self.depth_embedding = nn.Embedding(num_depths, d_model)   # 层级 embedding(随机初始化)

        # 3. 主干: 带 adaLN 的 block(条件 c = time + depth);blocks 内 self_attn/cross_attn/
        #    norm/ffn 与 ModernCrossAttnTransformerBlock 同名可迁移,adaLN_modulation 为新增(恒等初始化)
        self.depth = list(depth)
        _blk = lambda: AdaLNCrossAttnBlock(d_model, nhead, dropout=dropout, qk_norm=qk_norm, ffn_hidden=ffn_hidden)
        self.enc_blocks_9 = nn.ModuleList([_blk() for _ in range(depth[0])])
        self.enc_blocks_3 = nn.ModuleList([_blk() for _ in range(depth[1])])
        self.bottleneck_blocks = nn.ModuleList([_blk() for _ in range(depth[2])])
        self.dec_blocks_3 = nn.ModuleList([_blk() for _ in range(depth[1])])
        self.dec_blocks_9 = nn.ModuleList([_blk() for _ in range(depth[0])])
        self.down_9_to_3 = LinearDownsample(d_model, shorten_factor=3)
        self.down_3_to_1 = LinearDownsample(d_model, shorten_factor=3)
        self.up_1_to_3 = LinearUpsample(d_model, shorten_factor=3)
        self.up_3_to_9 = LinearUpsample(d_model, shorten_factor=3)
        self.rotary_emb = RotaryEmbedding(dim=d_model // nhead, max_position_embeddings=max_seq_len)

        # 4. 因子分解输出头 ------------------------------------------------
        self.f_head = nn.Linear(d_model, SUB_VOCAB)                          # F: 1 个子码(单头)
        # intra/inter: 8 行**自回归** MLP 头(行间依赖, 取代旧的 8 行并行独立 Linear)
        self.intra_head = AutoregressiveRowHead(d_model, num_rows=NUM_OCTANTS, vocab=SUB_VOCAB)
        self.inter_head = AutoregressiveRowHead(d_model, num_rows=NUM_OCTANTS, vocab=SUB_VOCAB)

        # 5. 点云条件 ------------------------------------------------------
        self.use_pc = use_pc
        if use_pc:
            print(f'[fix3] Point cloud encoder: {encoder_name} | freeze: {encoder_freeze}')
            self.conditioner = PointConditioner(model_name=encoder_name, freeze=encoder_freeze, feature_dim=d_model,
                                                num_latents=pc_num_latents)
            self.null_cond_len = 257
            self.null_cond = nn.Parameter(torch.randn(self.null_cond_len, d_model) * 0.02)

    # ------------------------------------------------------------------ #
    # 工具
    # ------------------------------------------------------------------ #
    def types_from_lengths(self, lengths):
        """由每个样本长度(=9*nface)推出 packed 的位置类型 [total],0/1/2 = F/intra/inter。"""
        parts = []
        for L in lengths.tolist():
            idx = torch.arange(L, device=lengths.device)
            fp = idx % REFINE_POS_PER_FACE
            t = torch.where(fp < 3, TYPE_F,
                            torch.where(fp < 6, TYPE_INTRA, TYPE_INTER))
            parts.append(t)
        return torch.cat(parts).long() if parts else torch.empty(0, dtype=torch.long, device=lengths.device)

    def embed_refine(self, subcodes, types):
        """subcodes [N,8] long, types [N] long -> [N,d]。
        F 位置: 单子码查表。intra/inter 位置: 8 行各查表 -> cat -> Linear(保留行序,避免 sum 丢失)。"""
        N = subcodes.size(0)
        out = self.f_sub_embedding.weight.new_zeros((N, self.d_model))
        is_f = types == TYPE_F
        is_in = types == TYPE_INTRA
        is_ix = types == TYPE_INTER
        # autocast(bf16) 下 Linear 输出是 bf16,而 out 是 fp32;原地 index_put 要求 dtype 一致,
        # 故统一 .to(out.dtype)(保持 refine_emb 与 raw_emb 一致的 fp32,backbone matmul 再由 autocast 转 bf16)。
        if is_f.any():
            out[is_f] = self.f_sub_embedding(subcodes[is_f, 0]).to(out.dtype)
        if is_in.any():
            e = self.intra_sub_embedding(subcodes[is_in])              # [n, 8, row_embed_dim]
            out[is_in] = self.intra_in_proj(e.reshape(e.size(0), -1)).to(out.dtype)  # cat -> Linear -> [n, d]
        if is_ix.any():
            e = self.inter_sub_embedding(subcodes[is_ix])
            out[is_ix] = self.inter_in_proj(e.reshape(e.size(0), -1)).to(out.dtype)
        return out

    # ------------------------------------------------------------------ #
    # 前向(varlen):返回主干 hidden h [total, d] 与 packed types [total]
    # ------------------------------------------------------------------ #
    def forward_varlen(self, coord_list, subcode_list, t, depth, pc=None):
        device = t.device
        coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
        subcode_list = [s.to(device=device, dtype=torch.long) for s in subcode_list]
        lengths = torch.tensor([c.numel() for c in coord_list], device=device, dtype=torch.long)
        if int(lengths.sum().item()) == 0:
            raise ValueError("forward_varlen received an empty batch.")
        if torch.any(lengths % REFINE_POS_PER_FACE != 0):
            raise ValueError("每个序列长度必须是 9 的倍数(每 face 9 个 refine 位置)。")

        coord_packed = torch.cat(coord_list, dim=0)
        subcode_packed = torch.cat(subcode_list, dim=0)
        types = self.types_from_lengths(lengths)

        raw_emb = self.raw_embedding(coord_packed)
        refine_emb = self.embed_refine(subcode_packed, types)
        x = raw_emb + refine_emb                         # 不再加法注入 time;time/depth 走 adaLN

        # adaLN 条件 c = time_emb + depth_emb(逐样本 [b,d]);各 hourglass 阶段按其长度展开到逐 token
        depth = depth.to(device=device, dtype=torch.long)
        c_sample = self.time_mlp(t) + self.depth_embedding(depth)   # [b, d_model]

        def c_for(cur_lengths):
            return c_sample.repeat_interleave(cur_lengths, dim=0)

        if self.use_pc:
            if pc is None:
                bsz = len(coord_list)
                null = self.null_cond
                cond = null.unsqueeze(0).expand(bsz, -1, -1).reshape(-1, null.size(-1))
                cond_lengths = torch.full((bsz,), self.null_cond_len, device=device, dtype=torch.long)
            else:
                pc = pc.to(device=device)
                cond = self.conditioner(pc)
                cond_lengths = torch.full((cond.size(0),), cond.size(1), device=device, dtype=torch.long)
                cond = cond.reshape(-1, cond.size(-1))
        else:
            cond = None
            cond_lengths = None

        def rope_fn_for(cur_lengths):
            position_ids = lengths_to_position_ids(cur_lengths)
            cos, sin = self.rotary_emb.forward_positions(position_ids, dtype=x.dtype)
            return lambda q, k: apply_rotary_pos_emb_varlen(q, k, cos, sin)

        rope_fn_9 = rope_fn_for(lengths)
        c9 = c_for(lengths)
        for block in self.enc_blocks_9:
            x = block.forward_varlen(x, c9, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_9)
        skip_9 = x
        lengths_9 = lengths

        x, lengths = self.down_9_to_3.forward_varlen(x, lengths)
        rope_fn_3 = rope_fn_for(lengths)
        c3 = c_for(lengths)
        for block in self.enc_blocks_3:
            x = block.forward_varlen(x, c3, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_3)
        skip_3 = x
        lengths_3 = lengths

        x, lengths = self.down_3_to_1.forward_varlen(x, lengths)
        rope_fn_1 = rope_fn_for(lengths)
        c1 = c_for(lengths)
        for block in self.bottleneck_blocks:
            x = block.forward_varlen(x, c1, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_1)

        x, lengths = self.up_1_to_3.forward_varlen(x, lengths)
        x = x + skip_3
        for block in self.dec_blocks_3:
            x = block.forward_varlen(x, c3, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_3)

        x, lengths = self.up_3_to_9.forward_varlen(x, lengths)
        x = x + skip_9
        for block in self.dec_blocks_9:
            x = block.forward_varlen(x, c9, lengths, cond, cond_lengths, rope_apply_fn=rope_fn_9)

        return x, types  # hidden [total,d], types [total]

    def forward(self, coord_list, noisy_subcode_list, t, depth,
                target_subcode_list=None, loss_mask=None, pc=None, return_extra_acc=False):
        """训练入口:把因子分解头放进 forward 内部(经 factorized_loss),保证 DDP 正确同步。
        depth: [b] long,每个样本的层级(target_depth),与 time 一起作为 adaLN 条件。
        - 传 target/loss_mask -> 返回 (loss, acc);return_extra_acc=True 时返回 (loss, acc, acc_nonempty)
          (acc_nonempty=连接非空行准确率,见 factorized_accuracy)。默认 False 与旧接口兼容。
        - 不传            -> 返回 (hidden, types)(推理/调试)
        """
        h, types = self.forward_varlen(coord_list, noisy_subcode_list, t, depth, pc)
        if target_subcode_list is None:
            return h, types
        target = torch.cat([s.to(device=h.device, dtype=torch.long) for s in target_subcode_list], dim=0)
        loss_mask = loss_mask.to(device=h.device)
        loss = self.factorized_loss(h, target, types, loss_mask)
        with torch.no_grad():
            acc, acc_ne = self.factorized_accuracy(h, target, types, loss_mask)
        if return_extra_acc:
            return loss, acc, acc_ne
        return loss, acc

    # ------------------------------------------------------------------ #
    # 因子分解 loss / accuracy
    # ------------------------------------------------------------------ #


    # ------------------------------------------------------------------ #
    # 采样(MaskGIT 风格,因子分解、varlen、逐位置置信度解 mask)
    # ------------------------------------------------------------------ #
    @torch.no_grad()
    def sample(self, coord_list, depth, pc=None, T_infer=10, cfg_scale=1.0, temperature=0.0,
               cfg_schedule="constant", cfg_start=1.0):
        """给定 coord(粗层条件)与 depth(层级),从全 MASK 迭代生成 refine 子码。
        depth: [b] long(每样本层级)。返回 subcode_list(list[ [L,8] ])。
        cfg_scale: classifier-free guidance 强度,约定 logits = uncond + cfg_scale*(cond - uncond)。
                   =1.0 即纯条件(无引导,跳过无条件前向);>1 放大点云条件(锐化连接预测、对抗欠连接)。
                   仅在 use_pc 且 pc 提供时生效(无条件分支用 pc=None 走训练过的 null_cond)。
        temperature: <=0 argmax 贪心(确定性,默认,与旧行为一致);>0 多项式随机采样。
                   best-of-N 时需 >0(如 1.0) 才能采出多样候选,否则贪心 N 次结果完全相同。
        cfg_schedule: "constant"=全程恒定 cfg_scale(默认,旧行为);"linear"=按步从 cfg_start
                   线性升到 cfg_scale(MUSE 式 ramp)。最初几步决定全局结构,CFG 锐化会把条件
                   敏感(=分歧大、没把握)位置的置信度人为抬高、过早 commit 且不可回滚;低 cfg
                   起步可避开这种早期过冲,引导力留给后期细化。
        cfg_start: ramp 起点(仅 linear 生效)。默认 1.0 = 第一步纯条件(并跳过该步无条件前向)。"""
        self.eval()
        device = next(self.parameters()).device
        coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
        depth = depth.to(device=device, dtype=torch.long)
        lengths = torch.tensor([c.numel() for c in coord_list], device=device, dtype=torch.long)
        types = self.types_from_lengths(lengths)
        want_cfg = (float(cfg_scale) != 1.0 or float(cfg_start) != 1.0) and self.use_pc and (pc is not None)

        total = int(lengths.sum().item())
        subcodes = torch.full((total, NUM_OCTANTS), MASK_ID, dtype=torch.long, device=device)
        decided = torch.zeros(total, dtype=torch.bool, device=device)

        for step in range(T_infer):
            # 当步实际引导强度:linear 时 step0 = cfg_start,末步 = cfg_scale
            if cfg_schedule == "linear":
                cfg_t = float(cfg_start) + (float(cfg_scale) - float(cfg_start)) * step / max(T_infer - 1, 1)
            else:
                cfg_t = float(cfg_scale)
            use_cfg = want_cfg and (cfg_t != 1.0)

            ratio = step / T_infer
            t = torch.full((len(coord_list),), ratio, device=device, dtype=torch.float)
            sub_list = list(torch.split(subcodes, lengths.tolist(), dim=0))
            h, _ = self.forward_varlen(coord_list, sub_list, t, depth, pc)
            h_u = self.forward_varlen(coord_list, sub_list, t, depth, None)[0] if use_cfg else None

            # 逐类型预测子码 + 置信度(该位置有效子码的平均最大概率)
            pred = subcodes.clone()
            conf = torch.zeros(total, device=device)
            # F: 单头(可 CFG)
            mf = types == TYPE_F
            if mf.any():
                logits = self.f_head(h[mf])                              # [nm,256]
                if use_cfg:
                    lu = self.f_head(h_u[mf])
                    logits = lu + cfg_t * (logits - lu)
                if temperature and temperature > 0:
                    prob = (logits / temperature).softmax(-1)
                    am = torch.multinomial(prob.float(), 1).squeeze(-1)              # [nm]
                    pm = prob.gather(-1, am.unsqueeze(-1)).squeeze(-1)
                else:
                    prob = logits.softmax(-1)
                    pm, am = prob.max(-1)                                # [nm],[nm]
                pred[mf, :] = am.unsqueeze(1).expand(-1, NUM_OCTANTS)    # F 复制到 8 槽
                conf[mf] = pm.to(conf.dtype)
            # intra/inter: 8 行自回归 rollout(可 CFG, cond/uncond 在每行 logits 级合并)
            for ty, head in ((TYPE_INTRA, self.intra_head), (TYPE_INTER, self.inter_head)):
                m = types == ty
                if not m.any():
                    continue
                p, pmaxs = head.rollout(h[m], h_u[m] if use_cfg else None, cfg_t, temperature)  # [nm,8],[nm,8]
                pred[m] = p
                conf[m] = pmaxs.mean(dim=1).to(conf.dtype)   # autocast 下 pmax 可能是 bf16

            # 已决定的位置保持不变
            keep_ratio = cosine_schedule(torch.tensor(float(step + 1)), float(T_infer)).item()
            n_keep_masked = int(keep_ratio * total)  # 仍保持 mask 的目标数(随步数减少)
            conf_for_select = conf.clone()
            conf_for_select[decided] = float('inf')   # 已定的不再参与"新解mask"
            # 选出本步要确定的位置:未定中置信度最高的若干个
            n_to_decide = max(0, (total - n_keep_masked) - int(decided.sum().item()))
            if n_to_decide > 0:
                undecided_idx = (~decided).nonzero(as_tuple=True)[0]
                order = conf[undecided_idx].argsort(descending=True)
                chosen = undecided_idx[order[:n_to_decide]]
                subcodes[chosen] = pred[chosen]
                decided[chosen] = True
            # 未决定位置回到 MASK,已决定保持
            subcodes[~decided] = MASK_ID

        # 收尾:还没定的全部按当前预测填上
        subcodes[~decided] = pred[~decided]
        return list(torch.split(subcodes, lengths.tolist(), dim=0))

# ==========================================================================
# 原 model_acc_fix3_altpc.py
# ==========================================================================

_BF16_RESIDUAL = os.environ.get("BF16_RESIDUAL", "1") == "1"
_STATIC_PAD_BUCKETS = tuple(sorted(int(x) for x in os.environ.get("STATIC_PAD_BUCKETS", "").split(",") if x.strip()))
_STATIC_PAD_SLOTS = int(os.environ.get("STATIC_PAD_SLOTS", "16"))
assert all(b % 9 == 0 for b in _STATIC_PAD_BUCKETS), "STATIC_PAD_BUCKETS 必须都是 9 的倍数"
class GeoRoPE:
    """几何 RoPE(rope_mode="geo"/"geo_vcat")的相位计算器。纯计算、零参数(不进 state_dict)。
    token 的"位置" = 3D 锚点 + 面内类型, 序列顺序完全不参与注意力(模型对面片块置换严格等变)。

    mode="geo": 锚点用聚合点(旧线 ckpt 在用, 勿删):
        9/face: F_i/M_ii 锚在父顶点 v_i, M_ij 锚在边中点 (v_i+v_j)/2; 类型 0/1/2=F/intra/inter
        3/face: 三个组 token 同锚面质心, 类型 = 组 id 0/1/2(与 9/face 同一条语义轴)
        1/face: 面质心, 类型 = 3
      head_dim/2 个频率槽切 [n,n,n,type_slots](n=(d/2-type_slots)//3, 余数补恒等相位),
      依序旋转 x/y/z/type(LATO RotaryPositionPhasesEmbedder 的 rotate_half 版)。
    mode="geo_vcat"(2026-08-05, 用户提议): 锚点显式编码顶点, **不用质心/中点等聚合点**。
        3/face 与 1/face 粗尺度: 「三顶点各占 1/3 通道拼接」(共质心三角形可分):
          xyz 30 槽切 3 组 x 每顶点 9 槽(x/y/z 各 3, 顶点序=面内 zyx 字典序), type 2 槽, 余 3 pad。
        9/face: 「双端点布局」(治中点混叠: 平行四边形两对角线同中点会被 geo 判成同一条边):
          每 token 两锚点各占一半 xyz 槽(x/y/z 各 5), F_i/M_ii=(v_i,v_i) 复制, M_ij=(v_i,v_j)
          (zyx 排序 => 端点对全局规范, 共享边跨面同相位); F 与其所属边的 inter 前半通道对齐。
      频率梯子统一 theta_vcat=300(3 槽/轴最低频 2.85 rad、5 槽/轴 1.33 rad, 均 <π 无歧义,
      邻格 1 rad 可分辨)。与 geo ckpt 参数完全同构, 但两个尺度的注意力几何都变了 ->
      热启动 loss 先跳后收(同 flat->geo 的迁移形态)。

    坐标约定(两种模式共同):
      锚点用**原始体素坐标**(乘 geo_scale, 默认 1.0), 刻意不按 depth 归一:
        注意力只见同样本内的相对相位(每样本单一 depth), 而连接决策的几何是体素邻接 ——
        体素单位让"相邻一格"在所有 depth 下相位签名一致(θ_xyz=100: 邻格 1 rad 可分辨,
        depth7 全程 128 格最低频 2.0 rad < π 无歧义)。
      coord 不减 offset: 常数平移让 q/k 同转一个角, 相对相位(注意力唯一可见量)不变。
      纯位置编码零新增参数 -> 任意旧 ckpt strict 热启动(注意几何变了, loss 先跳后收)。
    """

    def __init__(self, mode="flat", geo_scale=1.0, theta_xyz=100.0,
                 theta_type=10.0, type_slots=2, theta_vcat=300.0):
        self.mode = mode
        self.geo_scale = float(geo_scale)
        self.theta_xyz = float(theta_xyz)
        self.theta_type = float(theta_type)
        self.type_slots = int(type_slots)
        self.theta_vcat = float(theta_vcat)

    @property
    def is_geo(self):
        return str(self.mode).startswith("geo")

    @staticmethod
    def _ladder(theta, n, device):
        """几何频率梯子: freq_i = 1/theta^(i/n), i=0..n-1。"""
        return 1.0 / (theta ** (torch.arange(n, device=device, dtype=torch.float32) / max(n, 1)))

    @staticmethod
    def _cos_sin(rot, dtype):
        """[N, d_half] 相位 -> ([N,1,head_dim] cos, sin): 频率复制两半配 rotate_half,
        布局与 RotaryEmbedding.forward_positions 一致, apply_rotary_pos_emb_varlen 原样可用。"""
        emb = torch.cat((rot, rot), dim=-1)
        return emb.cos().to(dtype).unsqueeze(1), emb.sin().to(dtype).unsqueeze(1)

    def scales(self, coord_packed, d_half, dtype):
        """由 coord 流现算三个 hourglass 尺度的 (cos, sin), 返回 {9:(cos,sin), 3:..., 1:...}。
        每面 9 个 coord 标量 = 3 父顶点 x 3 分量(面内坐标字典序); 样本长度都是 9 的倍数且
        face 块不跨样本, 所以可以全局 view。"""
        device = coord_packed.device
        V = coord_packed.view(-1, 3, 3).to(torch.float32) * self.geo_scale   # [nF, 3顶点, xyz]
        nF = V.shape[0]
        freqs_type = self._ladder(self.theta_type, self.type_slots, device)
        # 面内类型分量(与 token 排布一致): 9/face = F F F | intra x3 | inter x3;
        # 3/face = 组 id 0/1/2(与 9/face 同一条语义轴); 1/face = 3
        type9 = torch.tensor([0., 0., 0., 1., 1., 1., 2., 2., 2.], device=device).repeat(nF)
        type3 = torch.tensor([0., 1., 2.], device=device).repeat(nF)
        type1 = torch.full((nF,), 3.0, device=device)
        if getattr(self, "mode", "geo") == "geo_vcat":
            return self._scales_vcat(V, type9, type3, type1, freqs_type, d_half, dtype, device)
        return self._scales_geo(V, type9, type3, type1, freqs_type, d_half, dtype, device)

    def _scales_geo(self, V, type9, type3, type1, freqs_type, d_half, dtype, device):
        """geo 布局: [x*n_ax | y*n_ax | z*n_ax | type*type_slots | pad]。"""
        n_type = self.type_slots
        n_ax = (d_half - n_type) // 3
        n_pad = d_half - n_type - 3 * n_ax
        freqs_ax = self._ladder(self.theta_xyz, n_ax, device)

        nF = V.shape[0]
        mid01 = (V[:, 0] + V[:, 1]) * 0.5
        mid02 = (V[:, 0] + V[:, 2]) * 0.5
        mid12 = (V[:, 1] + V[:, 2]) * 0.5
        centroid = V.mean(dim=1)                                        # [nF,3]
        # 槽位序 = [F0 F1 F2 M00 M11 M22 M01 M02 M12](与 token 排布一致)
        anchors9 = torch.stack([V[:, 0], V[:, 1], V[:, 2],
                                V[:, 0], V[:, 1], V[:, 2],
                                mid01, mid02, mid12], dim=1)            # [nF,9,3]
        anchors3 = centroid[:, None, :].expand(nF, 3, 3)                # down_9_to_3 按连续三元折叠成组
        anchors1 = centroid[:, None, :]

        def build(anchors, type_comp):
            a = anchors.reshape(-1, 3)
            parts = [a[:, 0:1] * freqs_ax, a[:, 1:2] * freqs_ax, a[:, 2:3] * freqs_ax,
                     type_comp[:, None] * freqs_type]
            if n_pad:
                parts.append(torch.zeros(a.shape[0], n_pad, device=device, dtype=torch.float32))
            return self._cos_sin(torch.cat(parts, dim=-1), dtype)

        return {9: build(anchors9, type9), 3: build(anchors3, type3), 1: build(anchors1, type1)}

    def _scales_vcat(self, V, type9, type3, type1, freqs_type, d_half, dtype, device):
        """geo_vcat: 锚点全部为显式顶点(不算质心/中点)。"""
        n_type = self.type_slots

        # ---- 粗尺度(3/face 与 1/face) = 三顶点拼接: 每顶点每轴 n_per_vert 槽,
        # 布局 [v0: x,y,z | v1: x,y,z | v2: x,y,z | type | pad]
        n_per_vert = (d_half - n_type) // 9
        n_pad_v = d_half - n_type - 9 * n_per_vert
        freqs_vcat = self._ladder(self.theta_vcat, n_per_vert, device)

        def build_vcat(type_comp, reps):
            parts_xyz = [V[:, vtx, ax:ax + 1] * freqs_vcat for vtx in range(3) for ax in range(3)]
            rot_face = torch.cat(parts_xyz, dim=-1)                     # [nF, 9*n_per_vert]
            rot = rot_face.repeat_interleave(reps, dim=0) if reps > 1 else rot_face
            parts = [rot, type_comp[:, None] * freqs_type]
            if n_pad_v:
                parts.append(torch.zeros(rot.shape[0], n_pad_v, device=device, dtype=torch.float32))
            return self._cos_sin(torch.cat(parts, dim=-1), dtype)

        # ---- 9/face = 双端点布局: 每 token 两个锚点各占一半 xyz 槽
        # [P1: x/y/z 各 n_ax9 | P2: 同 | type n_type], 30=2x15 无 pad。
        # F_i/M_ii -> (v_i, v_i) 复制; M_ij -> (v_i, v_j), i<j(面内 zyx 排序 => 端点对全局规范,
        # 共享边跨面同相位)。免费性质: F 锚 v_a 与 inter 锚 (v_a,v_b) 前半通道完全对齐 ——
        # "顶点是边的端点"在相位空间直接可见。
        n_ax9 = (d_half - n_type) // 6
        n_pad9 = d_half - n_type - 6 * n_ax9
        freqs_9 = self._ladder(self.theta_vcat, n_ax9, device)
        P1 = torch.stack([V[:, 0], V[:, 1], V[:, 2], V[:, 0], V[:, 1], V[:, 2],
                          V[:, 0], V[:, 0], V[:, 1]], dim=1).reshape(-1, 3)   # [nF*9,3]
        P2 = torch.stack([V[:, 0], V[:, 1], V[:, 2], V[:, 0], V[:, 1], V[:, 2],
                          V[:, 1], V[:, 2], V[:, 2]], dim=1).reshape(-1, 3)
        parts9 = [P1[:, 0:1] * freqs_9, P1[:, 1:2] * freqs_9, P1[:, 2:3] * freqs_9,
                  P2[:, 0:1] * freqs_9, P2[:, 1:2] * freqs_9, P2[:, 2:3] * freqs_9,
                  type9[:, None] * freqs_type]
        if n_pad9:
            parts9.append(torch.zeros(P1.shape[0], n_pad9, device=device, dtype=torch.float32))
        scales9 = self._cos_sin(torch.cat(parts9, dim=-1), dtype)
        return {9: scales9, 3: build_vcat(type3, 3), 1: build_vcat(type1, 1)}
_COORD_PACK_BASE = 4096   # 坐标打包基数(> 最大体素坐标), xyz -> 单一整数 code
def _pack_coord(p3):
    """[.,3] 整数 xyz -> 单一整数 code(供 torch.unique 分组用)。"""
    return p3[:, 0] * _COORD_PACK_BASE * _COORD_PACK_BASE + p3[:, 1] * _COORD_PACK_BASE + p3[:, 2]
class TokenGroup:
    """一个跨面 token 组划分 + 组内表决。
    字段: pos=该类型 token 的全局位置; group=每 token 的组 id(0..n_group-1);
          face=每 token 所在面; code=父顶点打包坐标(仅 by_parent, 顶点原子揭示要用)。"""

    def __init__(self, pos, group, n_group, face, code=None):
        self.pos, self.group, self.n_group, self.face, self.code = pos, group, n_group, face, code

    @classmethod
    def by_parent(cls, types, face_coords, token_type, parent_offset):
        """F/intra 按父顶点分组(同一顶点在不同面的 token 同组)。
        parent_offset: F=0(面内位置 0/1/2 -> 父 v0/v1/v2), intra=3(位置 3/4/5)。"""
        pos = (types == token_type).nonzero(as_tuple=True)[0]
        face = pos // REFINE_POS_PER_FACE
        code = _pack_coord(face_coords[face, (pos % REFINE_POS_PER_FACE) - parent_offset])
        uniq, group = torch.unique(code, return_inverse=True)
        return cls(pos, group, int(uniq.numel()), face, code)

    @classmethod
    def by_edge(cls, types, face_coords):
        """inter 按「无序父对(=共享边)」分组: 面内位置 6/7/8 -> 父对 (v0,v1)/(v0,v2)/(v1,v2)。
        父对取 (小,大) => 同一条边跨面同组, 朝向天然规范(无 M vs M^T 问题)。"""
        device = face_coords.device
        pos = (types == TYPE_INTER).nonzero(as_tuple=True)[0]
        face = pos // REFINE_POS_PER_FACE
        local = (pos % REFINE_POS_PER_FACE) - 6                     # 0/1/2 -> M01/M02/M12
        pa = torch.tensor([0, 0, 1], device=device)
        pb = torch.tensor([1, 2, 2], device=device)
        code_a = _pack_coord(face_coords[face, pa[local]])
        code_b = _pack_coord(face_coords[face, pb[local]])
        pair = torch.stack([torch.minimum(code_a, code_b), torch.maximum(code_a, code_b)], dim=1)
        uniq, group = torch.unique(pair, dim=0, return_inverse=True)
        return cls(pos, group, int(uniq.shape[0]), face)

    @staticmethod
    def _mode_of(group, vid, n_group):
        """每组内 vid 的众数(sort+segment, 免 python 循环), 广播回每个成员。"""
        n_vid = int(vid.max().item()) + 1 if vid.numel() else 1
        key, cnt = torch.unique(group.long() * n_vid + vid.long(), return_counts=True)
        g, v = key // n_vid, key % n_vid
        order = torch.argsort(g * (int(cnt.max().item()) + 1) - cnt)   # 组内按计数降序
        g, v = g[order], v[order]
        first = torch.ones_like(g, dtype=torch.bool)
        if g.numel() > 1:
            first[1:] = g[1:] != g[:-1]
        mode = torch.zeros(n_group, dtype=torch.long, device=vid.device)
        mode[g[first]] = v[first]
        return mode[group]

    def vote_bytes(self, vals):
        """F 通道: 每组对占用字节(0..255)计数表决, 返回广播回每 token 的众数字节。"""
        cnt = torch.bincount(self.group * 256 + vals.clamp(0, 255),
                             minlength=self.n_group * 256).view(self.n_group, 256)
        return cnt.argmax(1)[self.group]

    def vote_vec(self, vecs):
        """intra/inter 通道: 8-subcode 向量整体表决。返回 (该 token 是否≠众数, 广播回的众数向量)。"""
        uniq, vid = torch.unique(vecs, dim=0, return_inverse=True)
        mode_vid = self._mode_of(self.group, vid, self.n_group)
        return vid != mode_vid, uniq[mode_vid]
class _DualCloudCond:
    """一次前向内的点云条件: A=全GT云, B=目标分辨率塌缩云, 按全局 block 序号交替提供 cross-attn KV
    (奇数层->A, 偶数层->B; B 未提供时两套相同 = 原单点云行为; 云为 None 走 null_cond = CFG 无条件分支)。
    每套 cond = (packed KV [sum(len),d], 每样本 KV 长度, KV max_len)。
    fake_lens 桶 pad 时在 conditioner **之后**补零行(不浪费 Michelangelo 前向)。
    cache: 推理跨步复用编码(采样各步点云不变, miche 逐步重算纯浪费; 数值严格等价), 训练传 None。"""

    def __init__(self, model, coord_list, fake_lens, device, pc, pc_target, cache=None):
        self._model = model
        self._n_seq = len(coord_list)          # 含 fake 序列(null_cond 也要按批展开)
        self._fake_lens = fake_lens
        self._device = device
        key = ("null" if pc is None else "cond", pc_target is None)
        if cache is not None and key in cache:
            self.cond_a, self.cond_b = cache[key]
            return
        self.cond_a = self._encode(pc)                               # 奇数层(全GT)
        if model.use_pc and (pc_target is not None):
            self.cond_b = self._encode(pc_target)                    # 偶数层(目标分辨率塌缩)
        else:
            self.cond_b = self.cond_a                                # 未提供 -> 两套相同(原行为)
        if cache is not None:
            cache[key] = (self.cond_a, self.cond_b)

    def _encode(self, cloud):
        model, device = self._model, self._device
        if not model.use_pc:
            return None, None, None
        if cloud is None:                                            # CFG 无条件分支
            null = model.null_cond
            kv = null.unsqueeze(0).expand(self._n_seq, -1, -1).reshape(-1, null.size(-1))
            kv_len = torch.full((self._n_seq,), model.null_cond_len, device=device, dtype=torch.long)
            return kv, kv_len, int(model.null_cond_len)
        emb = model.conditioner(cloud.to(device=device))
        if self._fake_lens:                                          # 假序列的 cross-attn KV = 零行
            emb = torch.cat([emb, emb.new_zeros(len(self._fake_lens), emb.size(1), emb.size(2))], dim=0)
        kv_len = torch.full((emb.size(0),), emb.size(1), device=device, dtype=torch.long)
        return emb.reshape(-1, emb.size(-1)), kv_len, int(emb.size(1))

    def for_layer(self, gi):
        """gi: 0-based 全局 block 序号; 层号=gi+1 -> 奇数层(gi 偶)用 A, 偶数层(gi 奇)用 B。"""
        return self.cond_a if gi % 2 == 0 else self.cond_b
class _ReMDM:
    """ReMDM(arXiv 2503.00307, 训练免费): 步首以 σ_t 重掩已定 token, 本步前向重新预测;
    揭示额度按 n_keep_masked 自动补足 -> 掩码数轨迹严格贴原 cosine 调度(边际保持的计数空间等价)。
    σ_max=min(1, m_s/|C|); cap: σ=min(η,σ_max); rescale: σ=η·σ_max; conf: cap 的量+定向重掩
    提交置信度最低者。env 门控(sample 调用时读取), REMDM 为空 = 与原行为逐位一致。"""

    def __init__(self):
        self.mode = os.environ.get("REMDM", "").strip().lower()
        self.eta = float(os.environ.get("REMDM_ETA", "0.05"))
        self.t_start = float(os.environ.get("REMDM_TSTART", "0.0"))
        self.t_end = float(os.environ.get("REMDM_TEND", "1.0"))

    def maybe_remask(self, step, T_infer, total, decided, subcodes, commit_conf, device):
        """就地重掩(decided/subcodes 原位改写)。"""
        if not (self.mode and step > 0 and self.t_start <= step / T_infer <= self.t_end):
            return
        n_committed = int(decided.sum().item())
        if n_committed == 0:
            return
        n_mask_target = int(cosine_schedule(torch.tensor(float(step + 1)), float(T_infer)).item() * total)
        sigma_max = min(1.0, n_mask_target / n_committed)
        sigma = self.eta * sigma_max if self.mode == "rescale" else min(self.eta, sigma_max)
        n_remask = int(sigma * n_committed)
        if n_remask <= 0:
            return
        committed_idx = decided.nonzero(as_tuple=True)[0]
        if self.mode == "conf":
            remask_idx = committed_idx[commit_conf[committed_idx].argsort()[:n_remask]]   # 最低提交置信优先
        else:
            remask_idx = committed_idx[torch.randperm(n_committed, device=device)[:n_remask]]
        decided[remask_idx] = False
        subcodes[remask_idx] = MASK_ID
class FactorizedDiffusionTransformerAltPC(FactorizedDiffusionTransformer):
    def __init__(self, *args, rope_mode="flat", rope_geo_scale=1.0,
                 rope_theta_xyz=100.0, rope_theta_type=10.0, rope_type_slots=2,
                 rope_theta_vcat=300.0, **kwargs):
        super().__init__(*args, **kwargs)
        # 几何 RoPE 封装在 GeoRoPE(纯计算无参数, 不影响 state_dict)。
        # rope_mode="flat"(默认): 原行为, 逐 token 序列位置 0..L-1, 与所有旧 ckpt 完全兼容;
        # "geo"/"geo_vcat" 的设计与 theta 约束见 GeoRoPE docstring。
        self.geo_rope = GeoRoPE(mode=rope_mode, geo_scale=rope_geo_scale,
                                theta_xyz=rope_theta_xyz, theta_type=rope_theta_type,
                                type_slots=rope_type_slots, theta_vcat=rope_theta_vcat)

    # rope_* 属性代理到 GeoRoPE(单一事实源): 外部读写 self.rope_mode 等保持兼容
    # (含子类/脚本事后改 rope_mode 的用法, 如 tssr 的 self.rope_mode = "hier")。
    rope_mode = property(lambda self: self.geo_rope.mode,
                         lambda self, v: setattr(self.geo_rope, "mode", v))
    rope_geo_scale = property(lambda self: self.geo_rope.geo_scale,
                              lambda self, v: setattr(self.geo_rope, "geo_scale", float(v)))
    rope_theta_xyz = property(lambda self: self.geo_rope.theta_xyz,
                              lambda self, v: setattr(self.geo_rope, "theta_xyz", float(v)))
    rope_theta_type = property(lambda self: self.geo_rope.theta_type,
                               lambda self, v: setattr(self.geo_rope, "theta_type", float(v)))
    rope_type_slots = property(lambda self: self.geo_rope.type_slots,
                               lambda self, v: setattr(self.geo_rope, "type_slots", int(v)))
    rope_theta_vcat = property(lambda self: self.geo_rope.theta_vcat,
                               lambda self, v: setattr(self.geo_rope, "theta_vcat", float(v)))

    def _geo_rope_scales(self, coord_packed, dtype):
        """geo/geo_vcat: 三个 hourglass 尺度的 (cos,sin)。方法名/签名保留(fpsq 子类在调用)。"""
        return self.geo_rope.scales(coord_packed, self.rotary_emb.inv_freq.numel(), dtype)

    @staticmethod
    def _static_pad_plan(total, b_real):
        """返回假序列长度列表(都是 9 的倍数且 >=9): padded 总数落到桶位、批槽位补满 _STATIC_PAD_SLOTS。
        超过最大桶位时返回 None(不 pad, 触发一次形状特化编译; 配 hard_max_positions 应当罕见)。"""
        n_fake = max(_STATIC_PAD_SLOTS - b_real, 1)
        min_pad = 9 * n_fake
        target = next((B for B in _STATIC_PAD_BUCKETS if B >= total + min_pad), None)
        if target is None:
            return None
        fake = [9] * n_fake
        fake[0] += target - total - min_pad          # 桶位/总数都是 9 的倍数 -> 余量也是
        return fake

    def forward_varlen(self, coord_list, subcode_list, t, depth, pc=None, pc_target=None,
                       cond_cache=None):
        device = t.device
        coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
        subcode_list = [s.to(device=device, dtype=torch.long) for s in subcode_list]
        # 长度在 python 端本来就知道 -> 一并算出各尺度 max_seqlen 传给 attention,
        # 省掉每个 flash 调用的 int(lengths.max().item()) GPU->CPU 同步(见 model_acc_fix3_tssr)。
        lengths_py = [int(c.numel()) for c in coord_list]
        if sum(lengths_py) == 0:
            raise ValueError("forward_varlen received an empty batch.")
        if any(n % REFINE_POS_PER_FACE != 0 for n in lengths_py):
            raise ValueError("每个序列长度必须是 9 的倍数(每 face 9 个 refine 位置)。")

        # ---- 静态桶化 padding(见文件顶部说明; STATIC_PAD_BUCKETS 为空 = 原行为) ----
        real_total, real_b = sum(lengths_py), len(lengths_py)
        fake_lens = self._static_pad_plan(real_total, real_b) if _STATIC_PAD_BUCKETS else None
        if fake_lens:
            sub_tail = tuple(subcode_list[0].shape[1:])
            for L in fake_lens:
                coord_list.append(torch.zeros(L, dtype=torch.long, device=device))
                subcode_list.append(torch.zeros((L,) + sub_tail, dtype=torch.long, device=device))
            lengths_py = lengths_py + fake_lens
        lengths = torch.tensor(lengths_py, device=device, dtype=torch.long)
        if fake_lens:
            # pad 模式: max_len 传桶位常数(= padded 总数, 合法上界), 保证编译形状/guard 全静态
            max_len_9 = sum(lengths_py)
        else:
            max_len_9 = max(lengths_py)
        max_len_3 = max_len_9 // 3          # hourglass 整除, 断言在上面
        max_len_1 = max_len_9 // 9

        coord_packed = torch.cat(coord_list, dim=0)
        subcode_packed = torch.cat(subcode_list, dim=0)
        types = self.types_from_lengths(lengths)

        raw_emb = self.raw_embedding(coord_packed)
        refine_emb = self.embed_refine(subcode_packed, types)
        x = raw_emb + refine_emb                         # 不再加法注入 time;time/depth 走 adaLN
        # 统一残差流 dtype(理由见文件顶部 _BF16_RESIDUAL 注释); c_sample 刻意保持 fp32(adaLN 惯例)
        if _BF16_RESIDUAL and torch.is_autocast_enabled():
            x = x.to(torch.get_autocast_dtype("cuda"))

        # adaLN 条件 c = time_emb + depth_emb(逐样本 [b,d]); 各 hourglass 阶段按其长度展开到逐 token
        depth = depth.to(device=device, dtype=torch.long)
        c_sample = self.time_mlp(t) + self.depth_embedding(depth)   # [b, d_model]
        if fake_lens:                                                # 假序列的 adaLN 条件 = 零行
            c_sample = torch.cat([c_sample, c_sample.new_zeros(len(fake_lens), c_sample.size(-1))], dim=0)

        def c_per_token(cur_lengths):
            return c_sample.repeat_interleave(cur_lengths, dim=0)

        # 点云条件: 双云编码 + 逐层交替选择(见 _DualCloudCond)
        cond = _DualCloudCond(self, coord_list, fake_lens, device, pc, pc_target, cond_cache)

        # geo 模式: 三个尺度的 cos/sin 一次算好(锚点只依赖 coord/depth, 与序列位置无关)
        geo_scales = (self._geo_rope_scales(coord_packed, x.dtype)
                      if str(getattr(self, "rope_mode", "flat")).startswith("geo") else None)

        def rope_fn_for(cur_lengths, tokens_per_face=9):
            if geo_scales is not None:
                cos, sin = geo_scales[tokens_per_face]
            else:
                position_ids = lengths_to_position_ids(cur_lengths)
                cos, sin = self.rotary_emb.forward_positions(position_ids, dtype=x.dtype)
            return lambda q, k: apply_rotary_pos_emb_varlen(q, k, cos, sin)

        # ---- hourglass 五段: enc9 -> enc3 -> bottleneck -> dec3 -> dec9 ----
        gi = 0   # 全局 block 序号(横跨五段, 决定该层注入云A还是云B)

        def run_stage(x, blocks, c_stage, stage_lengths, rope_fn, stage_max_len):
            nonlocal gi
            for block in blocks:
                cond_kv, cond_kv_len, cond_kv_max = cond.for_layer(gi)
                gi += 1
                x = block.forward_varlen(x, c_stage, stage_lengths, cond_kv, cond_kv_len,
                                         rope_apply_fn=rope_fn, max_len=stage_max_len,
                                         cond_max_len=cond_kv_max)
            return x

        rope_fn_9, c9 = rope_fn_for(lengths, 9), c_per_token(lengths)
        x = run_stage(x, self.enc_blocks_9, c9, lengths, rope_fn_9, max_len_9)
        skip_9 = x

        x, lengths = self.down_9_to_3.forward_varlen(x, lengths)
        rope_fn_3, c3 = rope_fn_for(lengths, 3), c_per_token(lengths)
        x = run_stage(x, self.enc_blocks_3, c3, lengths, rope_fn_3, max_len_3)
        skip_3 = x

        x, lengths = self.down_3_to_1.forward_varlen(x, lengths)
        rope_fn_1, c1 = rope_fn_for(lengths, 1), c_per_token(lengths)
        x = run_stage(x, self.bottleneck_blocks, c1, lengths, rope_fn_1, max_len_1)

        x, lengths = self.up_1_to_3.forward_varlen(x, lengths)
        x = run_stage(x + skip_3, self.dec_blocks_3, c3, lengths, rope_fn_3, max_len_3)

        x, lengths = self.up_3_to_9.forward_varlen(x, lengths)
        x = run_stage(x + skip_9, self.dec_blocks_9, c9, lengths, rope_fn_9, max_len_9)

        if fake_lens:                                                # 切掉假位置(不进头/loss)
            x, types = x[:real_total], types[:real_total]
        return x, types  # hidden [total,d], types [total]

    def forward(self, coord_list, noisy_subcode_list, t, depth,
                target_subcode_list=None, loss_mask=None, pc=None, pc_target=None, return_extra_acc=False):
        """训练入口, 与基类同, 只多传 pc_target(逐层交替的第二套点云)。"""
        h, types = self.forward_varlen(coord_list, noisy_subcode_list, t, depth, pc, pc_target)
        if target_subcode_list is None:
            return h, types
        target = torch.cat([s.to(device=h.device, dtype=torch.long) for s in target_subcode_list], dim=0)
        loss_mask = loss_mask.to(device=h.device)
        loss = self.factorized_loss(h, target, types, loss_mask)   # focal(继承)
        with torch.no_grad():
            acc, acc_ne = self.factorized_accuracy(h, target, types, loss_mask)
        if return_extra_acc:
            return loss, acc, acc_ne
        return loss, acc

    def _predict_heads(self, h, h_u, types, subcodes, cfg_t, temperature):
        """三个头对全部位置出预测 + 置信度: F=256 类单分类(整块 9 位广播同值),
        intra/inter=8 行自回归 rollout。h_u 非 None 时做 logits 级 CFG。返回 (pred, conf)。"""
        pred = subcodes.clone()
        conf = torch.zeros(types.numel(), device=h.device)
        is_f = types == TYPE_F
        if is_f.any():
            logits = self.f_head(h[is_f])
            if h_u is not None:
                logits_u = self.f_head(h_u[is_f])
                logits = logits_u + cfg_t * (logits - logits_u)
            if temperature and temperature > 0:
                prob = (logits / temperature).softmax(-1)
                choice = torch.multinomial(prob.float(), 1).squeeze(-1)
                choice_prob = prob.gather(-1, choice.unsqueeze(-1)).squeeze(-1)
            else:
                prob = logits.softmax(-1)
                choice_prob, choice = prob.max(-1)
            pred[is_f, :] = choice.unsqueeze(1).expand(-1, NUM_OCTANTS)
            conf[is_f] = choice_prob.to(conf.dtype)
        for token_type, head in ((TYPE_INTRA, self.intra_head), (TYPE_INTER, self.inter_head)):
            sel = types == token_type
            if not sel.any():
                continue
            row_pred, row_conf = head.rollout(h[sel], h_u[sel] if h_u is not None else None, cfg_t, temperature)
            pred[sel] = row_pred
            conf[sel] = row_conf.mean(dim=1).to(conf.dtype)
        return pred, conf

    @torch.no_grad()
    def sample(self, coord_list, depth, pc=None, pc_target=None, T_infer=10, cfg_scale=1.0,
               temperature=0.0, cfg_schedule="constant", cfg_start=1.0,
               align_fm=False, align_inter=False, align_per_step=False, align_debug=True,
               diag_vertex=False):
        """前向多传 pc_target(逐层交替)。CFG 无条件分支两套点云都走 null_cond。

        跨面一致性对齐(GT 侧这些量对共享结构 100% 一致, 见 check_tokenizer_consistency):
          align_fm=True    : 对齐 F(占用字节, 按"共享父"分组) + intra(M_inner 8-subcode 矩阵, 按"共享父")。
          align_inter=True : 对齐 inter(M_outer 8-subcode 矩阵, 按"共享父对"=共享边分组)。
                             如面 (v0,v1,v2) 与 (v0,v1,v3) 共享边 (v0,v1) -> 其 (v0,v1) 的 inter 应一致。
          align_per_step   : False(默认, 推荐)= 只在收尾把各启用通道投影到表决值(不扰动去噪, 不造洞);
                             True = 每步额外把"不一致的面"整块 9 token 重掩重预测(实测打乱揭示节奏 ->
                             反而多洞, 仅供对照)。
        全 False -> 与原行为逐位一致。

        env 门控(为空/未设 = 与原行为逐位一致):
          REMDM=cap|rescale|conf (+REMDM_ETA/TSTART/TEND): 步首重掩已定 token(见 _ReMDM)。
          WINDOW_REVEAL=1(仅 batch=1): BFS 前缀窗内 token 才可提交(块因果 变体A), 注意力/预测不变。
        """
        self.eval()
        device = next(self.parameters()).device
        coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
        depth = depth.to(device=device, dtype=torch.long)
        lengths = torch.tensor([c.numel() for c in coord_list], device=device, dtype=torch.long)
        types = self.types_from_lengths(lengths)
        want_cfg = (float(cfg_scale) != 1.0 or float(cfg_start) != 1.0) and self.use_pc and (pc is not None)

        total = int(lengths.sum().item())
        nface = total // REFINE_POS_PER_FACE

        align_any = align_fm or align_inter
        if align_any or diag_vertex:
            face_coords = torch.cat(coord_list).view(nface, 3, 3)   # [nface, 3父, xyz]

        # ---- 顶点组诊断: 同一父(顶点)在不同面的 F/intra token, 记录 commit step, 末尾统计一致性 ----
        if diag_vertex:
            diag_f = TokenGroup.by_parent(types, face_coords, TYPE_F, 0)
            diag_i = TokenGroup.by_parent(types, face_coords, TYPE_INTRA, 3)
            commit_step = torch.full((total,), int(T_infer), dtype=torch.long, device=device)

        # ---- 跨面对齐分组: F/intra 按"共享父", inter 按"共享父对"(=共享边) ----
        if align_fm:
            grp_f = TokenGroup.by_parent(types, face_coords, TYPE_F, 0)
            grp_i = TokenGroup.by_parent(types, face_coords, TYPE_INTRA, 3)
        if align_inter:
            grp_e = TokenGroup.by_edge(types, face_coords)

        subcodes = torch.full((total, NUM_OCTANTS), MASK_ID, dtype=torch.long, device=device)
        decided = torch.zeros(total, dtype=torch.bool, device=device)
        pred = subcodes.clone()

        remdm = _ReMDM()                                    # env 门控, 未设=无操作
        window_reveal = os.environ.get("WINDOW_REVEAL", "0") == "1" and len(lengths) == 1
        commit_conf = torch.zeros(total, device=device)     # 每 token 提交时的置信度(ReMDM-conf 用)
        cond_cache = {}                                     # 点云条件跨步缓存(严格等价, 见 _DualCloudCond)

        for step in range(T_infer):
            remdm.maybe_remask(step, T_infer, total, decided, subcodes, commit_conf, device)

            if cfg_schedule == "linear":
                cfg_t = float(cfg_start) + (float(cfg_scale) - float(cfg_start)) * step / max(T_infer - 1, 1)
            else:
                cfg_t = float(cfg_scale)
            use_cfg = want_cfg and (cfg_t != 1.0)

            # ---- 前向(有条件 + 可选无条件 CFG 分支) -> 三头全量预测 ----
            t = torch.full((len(coord_list),), step / T_infer, device=device, dtype=torch.float)
            sub_list = list(torch.split(subcodes, lengths.tolist(), dim=0))
            h, _ = self.forward_varlen(coord_list, sub_list, t, depth, pc, pc_target, cond_cache=cond_cache)
            h_u = self.forward_varlen(coord_list, sub_list, t, depth, None, None, cond_cache=cond_cache)[0] if use_cfg else None
            h = h.float()                                    # 头部权重 fp32; bf16 残差模式下需 cast(fp32 下 no-op)
            if h_u is not None:
                h_u = h_u.float()
            pred, conf = self._predict_heads(h, h_u, types, subcodes, cfg_t, temperature)

            # ---- 每步重掩(align_per_step): 跨面不一致的面整块 9 token 重掩(本步不揭示+撤销已定)。
            # 检测 F/intra(按共享父)与 inter(按共享父对)的不一致; 任一通道不一致的面都重掩。实测会造洞, 默认关。
            if align_per_step and align_any:
                force_face = torch.zeros(nface, dtype=torch.bool, device=device)
                if align_fm:
                    if grp_f.pos.numel():
                        bad = pred[grp_f.pos, 0] != grp_f.vote_bytes(pred[grp_f.pos, 0])
                        force_face[grp_f.face[bad]] = True
                    if grp_i.pos.numel():
                        dis_intra, _ = grp_i.vote_vec(pred[grp_i.pos])
                        force_face[grp_i.face[dis_intra]] = True
                if align_inter and grp_e.pos.numel():
                    dis_inter, _ = grp_e.vote_vec(pred[grp_e.pos])
                    force_face[grp_e.face[dis_inter]] = True
                remask = force_face.repeat_interleave(REFINE_POS_PER_FACE)
                conf[remask] = -float('inf')
                decided = decided & (~remask)

            # ---- 按 cosine 调度揭示 top-k 置信 token ----
            keep_ratio = cosine_schedule(torch.tensor(float(step + 1)), float(T_infer)).item()
            n_keep_masked = int(keep_ratio * total)
            n_to_decide = max(0, (total - n_keep_masked) - int(decided.sum().item()))
            if n_to_decide > 0:
                undecided_idx = (~decided).nonzero(as_tuple=True)[0]
                if window_reveal:
                    # 块序前沿窗口(推理期块因果 变体A): 仅允许 BFS 前缀窗内的 token 本步被提交。
                    # 注意力/预测不变(coord 脚手架全局可见), 只限制提交资格; 窗内不够本步预算时自适应扩窗。
                    n_blocks = total // 9
                    win_blocks = max(1, int(np.ceil(n_blocks * (step + 1) / float(T_infer))))
                    while True:
                        in_window = undecided_idx[undecided_idx < win_blocks * 9]
                        if len(in_window) >= n_to_decide or win_blocks >= n_blocks:
                            break
                        win_blocks += max(1, (n_to_decide - len(in_window) + 8) // 9)
                    undecided_idx = undecided_idx[undecided_idx < win_blocks * 9]
                order = conf[undecided_idx].argsort(descending=True)
                chosen = undecided_idx[order[:n_to_decide]]
                subcodes[chosen] = pred[chosen]
                decided[chosen] = True
                commit_conf[chosen] = conf[chosen]
                if diag_vertex:
                    commit_step[chosen] = step
            subcodes[~decided] = MASK_ID

        subcodes[~decided] = pred[~decided]
        if diag_vertex:
            self._diag_vertex_report(subcodes, commit_step, diag_f.pos, diag_f.group,
                                     diag_i.pos, diag_i.group, int(T_infer))

        # ---- 末步: 把各启用通道强制对齐到表决值(不扰动去噪, 推荐的对齐方式)。
        # align_debug=True: 打印每通道"与表决值不一致(即被改动)的 token 数 / 涉及面数"便于 debug。
        def report(tag, disagree, face_of):
            n_tok = int(disagree.sum().item())
            n_all = disagree.numel()
            n_face = int(torch.unique(face_of[disagree]).numel()) if n_tok else 0
            print(f"[align_debug] {tag:5s}: 不一致 {n_tok}/{n_all} token ({100 * n_tok / max(n_all, 1):.1f}%), 涉及 {n_face} 面")

        if align_fm:
            if grp_f.pos.numel():
                voted_f = grp_f.vote_bytes(subcodes[grp_f.pos, 0])
                if align_debug:
                    report("F", subcodes[grp_f.pos, 0] != voted_f, grp_f.face)
                subcodes[grp_f.pos, :] = voted_f.unsqueeze(1).expand(-1, NUM_OCTANTS)
            if grp_i.pos.numel():
                dis_intra, voted_intra = grp_i.vote_vec(subcodes[grp_i.pos])
                if align_debug:
                    report("intra", dis_intra, grp_i.face)
                subcodes[grp_i.pos, :] = voted_intra
        if align_inter and grp_e.pos.numel():
            dis_inter, voted_inter = grp_e.vote_vec(subcodes[grp_e.pos])
            if align_debug:
                report("inter", dis_inter, grp_e.face)
            subcodes[grp_e.pos, :] = voted_inter
        return list(torch.split(subcodes, lengths.tolist(), dim=0))


    @torch.no_grad()
    def _diag_vertex_report(self, subcodes, commit_step, f_pos, f_grp, i_pos, i_grp, T):
        """诊断: 同一父(顶点)在不同面的 F/intra token 是否一致, 以及不一致的时序来源。
        关键问题——不一致是(a)同步揭示时就分裂, 还是(b)已有同组 token commit(可见上下文)后仍预测错?"""
        def analyze(pos, grp, tag, is_vec):
            if pos.numel() == 0:
                print(f"[diag {tag}] 无 token")
                return
            if is_vec:   # intra: 8-subcode 向量 -> 唯一 id
                _, vid = torch.unique(subcodes[pos], dim=0, return_inverse=True)
                vals = vid.cpu().numpy()
            else:        # F: 占用字节
                vals = subcodes[pos, 0].clamp(0, 255).cpu().numpy()
            groups = grp.cpu().numpy()
            steps = commit_step[pos].cpu().numpy()
            order = np.argsort(groups, kind="stable")
            groups, vals, steps = groups[order], vals[order], steps[order]
            segs = np.split(np.arange(len(groups)), np.flatnonzero(np.diff(groups)) + 1)
            n_multi = n_incons = 0
            post_total = post_dis = 0                     # 后揭示事件(同组已有更早 commit): 与已定值 一致/不一致
            coreveal = postcommit = both = 0              # 不一致组来源分类
            spreads = []
            for seg in segs:
                if len(seg) < 2:
                    continue
                n_multi += 1
                v, s = vals[seg], steps[seg]
                inconsistent = len(set(v.tolist())) > 1
                first_step = int(s.min())
                early_vals = v[s == first_step]
                ref = np.bincount(early_vals).argmax()    # 最早 step 的众数值作参照
                coreveal_split = len(set(early_vals.tolist())) > 1
                later = v[s > first_step]
                post_total += len(later)
                post_dis += int((later != ref).sum())
                has_post = bool((later != ref).any())
                if inconsistent:
                    n_incons += 1
                    spreads.append(int(s.max() - s.min()))
                    if coreveal_split and has_post:
                        both += 1
                    elif coreveal_split:
                        coreveal += 1
                    else:
                        postcommit += 1
            pct_dis = 100 * post_dis / max(post_total, 1)
            med = int(np.median(spreads)) if spreads else 0
            print(f"[diag {tag}] 多面顶点组 {n_multi} | 最终不一致 {n_incons} ({100*n_incons/max(n_multi,1):.1f}%)")
            print(f"          后揭示token(同组已有commit) {post_total}: 不一致 {post_dis} ({pct_dis:.1f}%) <- 看到已定值仍预测错的比例")
            print(f"          不一致组来源: 同步揭示分裂 {coreveal} / 后揭示才分裂 {postcommit} / 两者 {both} | commit step 跨度中位 {med}/{T}")

        analyze(f_pos, f_grp, "F", False)
        analyze(i_pos, i_grp, "intra", True)

# ==========================================================================
# 原 model_acc_fix3_tssr.py
# ==========================================================================

_BF16_RESIDUAL = os.environ.get("BF16_RESIDUAL", "1") == "1"
class FactorizedTSSRTransformer(FactorizedDiffusionTransformerAltPC):
    def __init__(self, *args, mode_embed_scale=1000.0, rope_mode="flat",
                 rope_theta_face=10000.0, rope_theta_group=10000.0, rope_theta_member=10000.0, **kwargs):
        super().__init__(*args, **kwargs)
        # ---- 层级 RoPE 开关(移植自 Tencent-TSSR MultiLevelRoPE, 适配本仓库 rotate_half 约定) ----
        # rope_mode="flat"(默认): 原行为, 逐 token 平位置 0..L-1, 与所有旧 ckpt 完全兼容。
        # rope_mode="hier"    : head 维的 d/2 个频率槽切三段, 分别用三个结构分量旋转:
        #     face   段: p // tokens_per_face          (第几个面; 长程)
        #     group  段: 组内三元组编号                 (9/face 时 (p%9)//3: 0=F,1=intra,2=inter)
        #     member 段: 三元组内成员                   (9/face 时 p%3: 顶点/边槽位, F0↔M00 同相位)
        #   hourglass 三个尺度(9/3/1 token per face)各有对应分解(对齐 TSSR 的 token/vertex/face 三级)。
        #   纯位置编码无参数 -> 任意旧 ckpt 可直接热启动, 但注意几何变了, loss 会先跳再收。
        self.rope_mode = rope_mode
        self.rope_thetas = (float(rope_theta_face), float(rope_theta_group), float(rope_theta_member))
        d = self.d_model
        # 模式 flag 嵌入:标量 mode∈{0,1} -> 正弦 -> MLP -> [b,d](仿 TSSR DiscreteValueEmbeding)。
        # scale 大让 0 与 1 的正弦特征清晰可分;末层零初始化使初始 mode token≈0(warm-start 不破坏骨干)。
        self.mode_emb = nn.Sequential(
            SinusoidalPosEmb(d, scale=mode_embed_scale),
            nn.Linear(d, d), nn.SiLU(),
            nn.Linear(d, d),
        )
        nn.init.zeros_(self.mode_emb[-1].weight)
        nn.init.zeros_(self.mode_emb[-1].bias)
        # 逐 token 正确性分类器(detach 骨干)
        self.to_acc = nn.Sequential(
            nn.Linear(d, d // 2), nn.SiLU(),
            nn.Linear(d // 2, d // 4), nn.SiLU(),
            nn.Linear(d // 4, 1),
        )

    def _hier_cos_sin(self, position_ids, tokens_per_face, dtype):
        """层级 RoPE 的 cos/sin, 形状/布局与 RotaryEmbedding.forward_positions 一致([total, head_dim],
        频率复制两半以配 rotate_half), 因此 apply_rotary_pos_emb_varlen 原样可用。
        d/2 个频率槽按 TSSR 方式切 [n, n, d/2-2n](n=d//6), 依序分给 face/group/member 分量。"""
        device = position_ids.device
        d_half = self.rotary_emb.inv_freq.numel()                 # = head_dim // 2
        n = d_half // 3
        chunk_sizes = (n, n, d_half - 2 * n)
        p = position_ids
        if tokens_per_face == 9:
            comps = (p // 9, (p % 9) // 3, p % 3)
        elif tokens_per_face == 3:
            comps = (p // 3, p % 3, torch.zeros_like(p))
        elif tokens_per_face == 1:
            comps = (p, torch.zeros_like(p), torch.zeros_like(p))
        else:
            raise ValueError(f"tokens_per_face 只支持 9/3/1, 收到 {tokens_per_face}")
        rotations = []
        for size, theta, comp in zip(chunk_sizes, self.rope_thetas, comps):
            freqs = 1.0 / (theta ** (torch.arange(size, device=device, dtype=torch.float32) / max(size, 1)))
            rotations.append(comp.to(torch.float32)[:, None] * freqs[None, :])
        rot = torch.cat(rotations, dim=-1)                        # [total, d_half]
        emb = torch.cat((rot, rot), dim=-1)                       # [total, head_dim], 同 rotate_half 布局
        # [total, 1, head_dim]: 中间加 head 广播维, 与 forward_positions 的返回形状一致
        # (q/k 是 [total, nhead, d_head], 少了这一维会把 total 广播到 nhead 上直接报形状错)
        return emb.cos().to(dtype).unsqueeze(1), emb.sin().to(dtype).unsqueeze(1)

    # ------------------------------------------------------------------ #
    # forward_varlen:复制 AltPC 版,改动 = ①mode token 拼点云 context ②rope_mode=hier 时层级 RoPE
    # ------------------------------------------------------------------ #
    def forward_varlen(self, coord_list, subcode_list, t, depth, pc=None, pc_target=None, mode=None):
        device = t.device
        coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
        subcode_list = [s.to(device=device, dtype=torch.long) for s in subcode_list]
        # 长度在 python 端本来就知道 -> 一并算出各尺度的 max_seqlen, 传给 attention 省掉
        # 每次 flash 调用的 int(lengths.max().item()) 同步(一次前向 72 次, 见 ModernAttention 说明)。
        lengths_py = [int(c.numel()) for c in coord_list]
        if sum(lengths_py) == 0:
            raise ValueError("forward_varlen received an empty batch.")
        if any(n % REFINE_POS_PER_FACE != 0 for n in lengths_py):
            raise ValueError("每个序列长度必须是 9 的倍数(每 face 9 个 refine 位置)。")
        lengths = torch.tensor(lengths_py, device=device, dtype=torch.long)
        max_len_9 = max(lengths_py)
        max_len_3 = max_len_9 // 3          # hourglass: lengths // 3, 整除有断言保证
        max_len_1 = max_len_9 // 9

        coord_packed = torch.cat(coord_list, dim=0)
        subcode_packed = torch.cat(subcode_list, dim=0)
        types = self.types_from_lengths(lengths)

        raw_emb = self.raw_embedding(coord_packed)
        refine_emb = self.embed_refine(subcode_packed, types)
        x = raw_emb + refine_emb

        # nn.Embedding 不在 autocast 的转型名单里 -> 上面的 x 是 fp32。不显式降下来的话:
        #   ① enc_blocks_9 / dec_blocks_9(经 skip_9 带回)这 4 个块的残差流全程 fp32 —— 而它们
        #      正是 token 数最多的一档(hourglass 把其余降到 1/3、1/9), RMSNorm/modulate/残差加
        #      在最大的激活上按 fp32 跑, 内存流量白翻一倍;
        #   ② rope_fn_9 是按 `dtype=x.dtype` 建 cos/sin 的, x 是 fp32 就把 bf16 的 Q/K 提升回 fp32,
        #      导致 can_use_flash_attn 判否、掉进 SDPA 逐序列 python 循环(见 apply_rotary_pos_emb_varlen)。
        # 统一降到 autocast dtype, 让整条残差流和 RoPE 都是 bf16。
        if _BF16_RESIDUAL and torch.is_autocast_enabled():
            x = x.to(torch.get_autocast_dtype("cuda"))

        depth = depth.to(device=device, dtype=torch.long)
        # c_sample **刻意保持 fp32**(按混精惯例): 它只有 [b, d_model], 保高精度的成本可忽略, 而它经
        # adaLN 变成 shift/scale/gate 逐元素乘加到所有激活上 —— 误差是系统性的、不会被平均掉。
        # DiT / SD3 / Flux 都把时间嵌入与调制留在 fp32。对比: 点云 latent 是 cross-attn 的 K/V,
        # 紧接着就进 k_proj/v_proj 的 matmul(autocast 必然转 bf16), 且 flash-attn 不接受 fp32, 故 bf16。
        c_sample = self.time_mlp(t) + self.depth_embedding(depth)   # [b, d_model], fp32

        def c_for(cur_lengths):
            return c_sample.repeat_interleave(cur_lengths, dim=0)

        bsz = len(coord_list)
        # 模式 token(每样本一个),拼到点云 context 尾部(TSSR 式 flag 注入)
        mode_tok = None
        if mode is not None:
            if torch.is_tensor(mode):
                mode_vec = mode.to(device=device, dtype=torch.float).reshape(-1)
            else:
                mode_vec = torch.full((bsz,), float(mode), device=device)
            mode_tok = self.mode_emb(mode_vec).unsqueeze(1)          # [b,1,d]

        # ---- 点云条件:两套(全GT / 目标分辨率塌缩),逐层交替; 各拼上 mode token ----
        def _encode_cond(_pc):
            if not self.use_pc:
                return None, None, None
            if _pc is None:
                emb = self.null_cond.unsqueeze(0).expand(bsz, -1, -1)     # [b,257,d]
            else:
                _pc = _pc.to(device=device)
                emb = self.conditioner(_pc)                              # [b,257,d]
            if mode_tok is not None:
                emb = torch.cat([emb, mode_tok.to(emb.dtype)], dim=1)     # [b,258,d]
            ctx_len = int(emb.size(1))                  # 每样本 context 长度恒为 257(+1 = mode token)
            cl = torch.full((emb.size(0),), ctx_len, device=device, dtype=torch.long)
            return emb.reshape(-1, emb.size(-1)), cl, ctx_len

        cond_a, cond_a_len, cond_max_len = _encode_cond(pc)         # 奇数层(全GT)
        if self.use_pc and (pc_target is not None):
            cond_b, cond_b_len, _ = _encode_cond(pc_target)         # 偶数层(目标分辨率塌缩)
        else:
            cond_b, cond_b_len = cond_a, cond_a_len                # 未提供 -> 两套相同

        def _cond_for(gi):
            return (cond_a, cond_a_len) if (gi % 2 == 0) else (cond_b, cond_b_len)

        # geo/geo_vcat(2026-08-23, 接 sfocal 大模型 warm-start): 三尺度 cos/sin 由 coord 现算,
        # 复用父类 GeoRoPE(锚点/频率梯子与主线逐位一致); hier/flat 走原路径不变。
        geo_scales = (self._geo_rope_scales(coord_packed, x.dtype)
                      if str(getattr(self, "rope_mode", "flat")).startswith("geo") else None)

        def rope_fn_for(cur_lengths, tokens_per_face=9):
            if geo_scales is not None:
                cos, sin = geo_scales[tokens_per_face]
            elif self.rope_mode == "hier":
                position_ids = lengths_to_position_ids(cur_lengths)
                cos, sin = self._hier_cos_sin(position_ids, tokens_per_face, x.dtype)
            else:
                position_ids = lengths_to_position_ids(cur_lengths)
                cos, sin = self.rotary_emb.forward_positions(position_ids, dtype=x.dtype)
            return lambda q, k: apply_rotary_pos_emb_varlen(q, k, cos, sin)

        gi = 0
        rope_fn_9 = rope_fn_for(lengths, 9)
        c9 = c_for(lengths)
        for block in self.enc_blocks_9:
            cnd, cnl = _cond_for(gi); gi += 1
            x = block.forward_varlen(x, c9, lengths, cnd, cnl, rope_apply_fn=rope_fn_9,
                                     max_len=max_len_9, cond_max_len=cond_max_len)
        skip_9 = x

        x, lengths = self.down_9_to_3.forward_varlen(x, lengths)
        rope_fn_3 = rope_fn_for(lengths, 3)
        c3 = c_for(lengths)
        for block in self.enc_blocks_3:
            cnd, cnl = _cond_for(gi); gi += 1
            x = block.forward_varlen(x, c3, lengths, cnd, cnl, rope_apply_fn=rope_fn_3,
                                     max_len=max_len_3, cond_max_len=cond_max_len)
        skip_3 = x

        x, lengths = self.down_3_to_1.forward_varlen(x, lengths)
        rope_fn_1 = rope_fn_for(lengths, 1)
        c1 = c_for(lengths)
        for block in self.bottleneck_blocks:
            cnd, cnl = _cond_for(gi); gi += 1
            x = block.forward_varlen(x, c1, lengths, cnd, cnl, rope_apply_fn=rope_fn_1,
                                     max_len=max_len_1, cond_max_len=cond_max_len)

        x, lengths = self.up_1_to_3.forward_varlen(x, lengths)
        x = x + skip_3
        for block in self.dec_blocks_3:
            cnd, cnl = _cond_for(gi); gi += 1
            x = block.forward_varlen(x, c3, lengths, cnd, cnl, rope_apply_fn=rope_fn_3,
                                     max_len=max_len_3, cond_max_len=cond_max_len)

        x, lengths = self.up_3_to_9.forward_varlen(x, lengths)
        x = x + skip_9
        for block in self.dec_blocks_9:
            cnd, cnl = _cond_for(gi); gi += 1
            x = block.forward_varlen(x, c9, lengths, cnd, cnl, rope_apply_fn=rope_fn_9,
                                     max_len=max_len_9, cond_max_len=cond_max_len)

        return x, types

    # ------------------------------------------------------------------ #
    # 分类器工具
    # ------------------------------------------------------------------ #
    def to_acc_logit(self, h):
        """逐 token 正确性 logit [total];detach 骨干,分类器梯度不回流。"""
        return self.to_acc(h.detach()).squeeze(-1)

    @torch.no_grad()
    def _pos_correct(self, h, target, types):
        """逐位置正确性 label [total] bool:该 refine 位置的头 argmax(teacher-forced)是否==GT。
        F 比 slot0;intra/inter 比 8 行全等。"""
        total = h.size(0)
        correct = torch.zeros(total, dtype=torch.bool, device=h.device)
        mf = types == TYPE_F
        if mf.any():
            pred = self.f_head(h[mf]).argmax(-1)
            correct[mf] = pred == target[mf, 0]
        for ty, head in ((TYPE_INTRA, self.intra_head), (TYPE_INTER, self.inter_head)):
            m = types == ty
            if not m.any():
                continue
            tgt = target[m]
            pred = head.forward_teacher(h[m], tgt).argmax(-1)   # [n,8] teacher-forced
            correct[m] = (pred == tgt).all(dim=1)
        return correct

    # ------------------------------------------------------------------ #
    # 训练入口
    # ------------------------------------------------------------------ #
    def forward(self, coord_list, noisy_subcode_list, t, depth, target_subcode_list=None,
                loss_mask=None, pc=None, pc_target=None, task="mask", acc_wrong_weight=1.0):
        """DDP 单一入口(必须走 model(...) 才能触发梯度同步;自定义方法会绕过 DDP)。
        task="mask"   -> (loss, acc, acc_ne)          吸收腐蚀 CE(仅 masked 位置)
        task="nomask" -> (loss_nomask, loss_acc, frac_correct, cls_acc)  uniform 去噪 CE(全位置)+ 分类器 BCE
        """
        if task == "mask":
            return self.forward_mask(coord_list, noisy_subcode_list, t, depth,
                                     target_subcode_list, loss_mask, pc, pc_target)
        elif task == "nomask":
            return self.forward_nomask(coord_list, noisy_subcode_list, t, depth,
                                       target_subcode_list, pc, pc_target, acc_wrong_weight)
        raise ValueError(f"unknown task {task!r}")




    # ------------------------------------------------------------------ #
    # 采样工具
    # ------------------------------------------------------------------ #
    def _predict_fill(self, h, h_u, types, cfg_t, use_cfg, temperature):
        """由 hidden 出各类型头 argmax(可 CFG/温度),填满 subcodes [total,8]。"""
        total = h.size(0)
        pred = torch.zeros((total, NUM_OCTANTS), dtype=torch.long, device=h.device)
        mf = types == TYPE_F
        if mf.any():
            logits = self.f_head(h[mf])
            if use_cfg:
                lu = self.f_head(h_u[mf]); logits = lu + cfg_t * (logits - lu)
            if temperature and temperature > 0:
                prob = (logits / temperature).softmax(-1)
                am = torch.multinomial(prob.float(), 1).squeeze(-1)
            else:
                am = logits.argmax(-1)
            pred[mf, :] = am.unsqueeze(1).expand(-1, NUM_OCTANTS)
        for ty, head in ((TYPE_INTRA, self.intra_head), (TYPE_INTER, self.inter_head)):
            m = types == ty
            if not m.any():
                continue
            p, _ = head.rollout(h[m], h_u[m] if use_cfg else None, cfg_t, temperature)
            pred[m] = p
        return pred

    @torch.no_grad()
    def predict_subcodes(self, coord_list, sub_list, t, depth, pc=None, pc_target=None, mode=1,
                         cfg_scale=1.0, temperature=0.0):
        """一次前向 + 头 argmax 得填满的 subcodes [total,8](供自生成脏输入 / 单步预测)。"""
        lengths = torch.tensor([c.numel() for c in coord_list], device=t.device, dtype=torch.long)
        types = self.types_from_lengths(lengths)
        use_cfg = (float(cfg_scale) != 1.0) and self.use_pc and (pc is not None)
        h, _ = self.forward_varlen(coord_list, sub_list, t, depth, pc, pc_target, mode=mode)
        h_u = self.forward_varlen(coord_list, sub_list, t, depth, None, None, mode=mode)[0] if use_cfg else None
        return self._predict_fill(h, h_u, types, float(cfg_scale), use_cfg, temperature)

    @torch.no_grad()
    def sample_tssr(self, coord_list, depth, pc=None, pc_target=None, T_infer=10,
                    cfg_scale=1.0, cfg_schedule="constant", cfg_start=1.0,
                    mask_logits_lo=0.2, mask_logits_hi=0.9, temperature=0.0,
                    generator=None, return_stats=False):
        """TSSR 三前向解码。返回 list[[L,8]] local 子码(drop-in 父类 decode 路径)。
        每步:① mask 预测(mode=1)填满 -> ② no-mask 精修(mode=0)+ 同前向出置信 -> ③ re-mask 判错位置。
        末步不再 re-mask。"""
        self.eval()
        device = next(self.parameters()).device
        coord_list = [c.to(device=device, dtype=torch.long) for c in coord_list]
        depth = depth.to(device=device, dtype=torch.long)
        lengths = torch.tensor([c.numel() for c in coord_list], device=device, dtype=torch.long)
        types = self.types_from_lengths(lengths)
        want_cfg = (float(cfg_scale) != 1.0 or float(cfg_start) != 1.0) and self.use_pc and (pc is not None)
        total = int(lengths.sum().item())

        subcodes = torch.full((total, NUM_OCTANTS), MASK_ID, dtype=torch.long, device=device)
        sched = torch.linspace(float(mask_logits_lo), float(mask_logits_hi), max(T_infer, 1), device=device)
        n_remask_hist = []

        for step in range(T_infer):
            if cfg_schedule == "linear":
                cfg_t = float(cfg_start) + (float(cfg_scale) - float(cfg_start)) * step / max(T_infer - 1, 1)
            else:
                cfg_t = float(cfg_scale)
            use_cfg = want_cfg and (cfg_t != 1.0)
            t = torch.full((len(coord_list),), step / T_infer, device=device, dtype=torch.float)

            # ① mask 预测(mode=1)
            sub_list = list(torch.split(subcodes, lengths.tolist(), dim=0))
            h1, _ = self.forward_varlen(coord_list, sub_list, t, depth, pc, pc_target, mode=1)
            h1u = self.forward_varlen(coord_list, sub_list, t, depth, None, None, mode=1)[0] if use_cfg else None
            x_t = self._predict_fill(h1, h1u, types, cfg_t, use_cfg, temperature)

            # ② no-mask 精修(mode=0)+ 复用该前向出逐 token 置信
            sub_list2 = list(torch.split(x_t, lengths.tolist(), dim=0))
            h2, _ = self.forward_varlen(coord_list, sub_list2, t, depth, pc, pc_target, mode=0)
            h2u = self.forward_varlen(coord_list, sub_list2, t, depth, None, None, mode=0)[0] if use_cfg else None
            x_t = self._predict_fill(h2, h2u, types, cfg_t, use_cfg, temperature)
            acc = torch.sigmoid(self.to_acc(h2).squeeze(-1)).float()      # [total] ∈[0,1]

            # ③ re-mask 判错位置(末步不做)
            if step < T_infer - 1:
                mask_prob = ((1.0 - acc) * sched[step]).clamp(1e-6, 1.0 - 1e-6)
                if generator is not None:
                    r = torch.rand(total, device=device, generator=generator)
                else:
                    r = torch.rand(total, device=device)
                remask = r < mask_prob
                subcodes = x_t.clone()
                subcodes[remask] = MASK_ID
                n_remask_hist.append(int(remask.sum().item()))
            else:
                subcodes = x_t

        out = list(torch.split(subcodes, lengths.tolist(), dim=0))
        if return_stats:
            return out, {"n_remask_per_step": n_remask_hist, "total_pos": total}
        return out