File size: 55,408 Bytes
eb4a470
 
 
 
 
 
 
 
 
 
 
3b8a0de
eb4a470
 
 
6f797b1
 
 
3b8a0de
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
6f797b1
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
 
 
 
 
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
 
 
 
 
 
 
 
 
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
6f797b1
 
 
 
 
 
 
 
3b8a0de
 
 
 
6f797b1
 
 
 
3b8a0de
6f797b1
 
 
 
 
 
 
3b8a0de
 
 
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f797b1
 
 
 
 
3b8a0de
 
 
6f797b1
 
 
 
3b8a0de
 
 
 
 
 
 
 
 
 
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
6f797b1
 
 
 
 
 
 
 
3b8a0de
 
 
 
 
 
6f797b1
 
 
3b8a0de
6f797b1
 
 
 
 
3b8a0de
 
 
 
 
 
 
 
6f797b1
 
3b8a0de
 
6f797b1
 
 
 
 
 
 
 
 
3b8a0de
 
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
6f797b1
3b8a0de
 
6f797b1
3b8a0de
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
 
 
 
 
 
 
 
6f797b1
 
 
 
 
 
3b8a0de
6f797b1
 
3b8a0de
 
 
 
6f797b1
 
 
 
 
3b8a0de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f797b1
 
 
 
 
 
 
 
3b8a0de
6f797b1
 
 
 
 
 
3b8a0de
 
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
6f797b1
3b8a0de
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
6f797b1
 
 
 
 
 
3b8a0de
 
6f797b1
 
 
3b8a0de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6f797b1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b8a0de
 
6f797b1
 
 
 
 
 
 
 
 
 
 
3b8a0de
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
---
title: Math Under Llm
emoji: 🌖
colorFrom: gray
colorTo: green
sdk: gradio
sdk_version: 6.14.0
python_version: '3.13'
app_file: app.py
pinned: false
license: apache-2.0
short_description: 'Compute SVD of LLM Q/K/V weights directly from Hugging Face'
---

Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference

---
# Wang's Five Laws — LLM Spectral Analyzer
## 完整项目文档 README.md(6-Tab Gradio App)

---


# 🔬 Wang's Five Laws — LLM Spectral Analyzer

**静态分析 LLM 注意力权重,无需推理,无需 benchmark,直接评估推理能力。**

通过对 Q/K/V 权重矩阵做 SVD 分解,验证王氏五定律,
计算 Wang Score(= 1 − median SSR_QK),实现跨模型推理能力排行。

[![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19707844-blue)](https://doi.org/10.5281/zenodo.19707844)
[![HAL](https://img.shields.io/badge/HAL-hal--05609398-red)](https://hal.science/hal-05609398)
[![Wang's Law](https://img.shields.io/badge/Wang%27s%20Law-r%3D1-blue)](https://github.com/emis-framework/math-under-llm)

---

## 目录

1. [项目背景](#1-项目背景)
2. [王氏五定律速查](#2-王氏五定律速查)
3. [整体架构](#3-整体架构)
4. [目录结构](#4-目录结构)
5. [各层详细说明](#5-各层详细说明)
   - 5.1 [core 层](#51-core-层——计算引擎)
   - 5.2 [db 层](#52-db-层——数据持久化)
   - 5.3 [ui 层](#53-ui-层——用户界面)
   - 5.4 [app.py 入口](#54-apppy——主入口)
6. [数据库表结构](#6-数据库表结构)
7. [数据流全链路](#7-数据流全链路)
8. [函数调用关系图](#8-函数调用关系图)
9. [关键设计决策](#9-关键设计决策)
10. [部署说明](#10-部署说明)
11. [依赖清单](#11-依赖清单)
12. [改动历史](#12-改动历史)

---

## 1. 项目背景

传统评估 LLM 推理能力需要跑 benchmark(耗时、昂贵、可刷榜)。

本项目发现:**只看权重矩阵的奇异值分解(SVD)结构,
就能静态评估模型推理质量**,无需任何推理。

核心原理:
- 对每一层注意力的 Q、K、V 权重矩阵做 SVD
- 计算奇异值谱之间的相关性、形状残差、子空间对齐度
- 这些指标与模型推理能力高度相关(经多个模型验证)

**运行方式**:HTTP Range Request 直接读取 HuggingFace 远程权重,
无需下载整个模型(一个 14B 模型只需读取约 200MB 数据而非 28GB)。

---

## 2. 王氏五定律速查

| 定律     | 名称               | 公式                                | 理论极值   | 实测范围     |
| -------- | ------------------ | ----------------------------------- | ---------- | ------------ |
| 第一定律 | 谱线性对齐         | Pearson r(s_Q, s_K)                 | → 1        | 0.94~0.99    |
| 第二定律 | 谱形状残差         | SSR = mean\|ŝ_Q − ŝ_K\|             | → 0        | 0.006~0.016  |
| 第三定律 | 精度-深度约束      | L_max = min(L_info, L_quant, L_dyn) | 由精度决定 | FP16→16层    |
| 第四定律 | 输出子空间解耦     | cosU(U_Q,U_V) < 1/√d_head           | 超正交     | ~20%低于随机 |
| 第五定律 | 输入子空间随机正交 | cosV ≈ 1/√d_model                   | ≈随机基线  | 符合理论     |

**Wang Score = 1 − median(SSR_QK)**(越高越好,理论极值=1)

---

## 3. 整体架构

```
┌─────────────────────────────────────────────────────┐
│                      app.py                          │
│              主入口,组装所有 6 个 Tab                 │
│              启动时调用 init_db()                      │
└──────┬──────────────────────────────────────────────┘
       │ 调用

┌──────────────────────────────────────────────────────┐
│                     ui/ 层                            │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │
│  │tab_inspect  │  │tab_analyze  │  │tab_leaderbd │  │
│  │结构探测      │  │分析+写库     │  │排行榜        │  │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  │
│         │                │                │          │
│         └────────────────┼────────────────┘          │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │
│  │tab_database │  │ tab_plot    │  │ tab_tables  │  │
│  │数据库浏览    │  │ 作图导出    │  │ 论文表格     │  │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  │
│         └────────────────┼────────────────┘          │
└──────┬──────────────────────────┬───────────────────┘
       │ 调用                      │ 调用
       ▼                          ▼
┌─────────────────┐    ┌─────────────────────────────┐
│    core/ 层      │    │          db/ 层              │
│                 │    │                             │
│ fetcher.py      │    │  schema.py  writer.py       │
│ 远程读取权重     │    │  建表       写入数据          │
│                 │    │                             │
│ layer_profile.py│    │  reader.py                  │
│ 推断层结构       │    │  查询数据                    │
│                 │    │                             │
│ metrics.py      │    │  SQLite 文件                 │
│ 计算五定律       │    │  /data/wang_laws.db          │
│                 │    │                             │
│ plotter.py      │    │                             │
│ matplotlib静态图 │    │                             │
│                 │    │                             │
│ plotter_plotly  │    │                             │
│ Plotly交互图    │    │                             │
│                 │    │                             │
│ table_gen.py    │    │                             │
│ 论文表格生成     │    │                             │
└─────────────────┘    └─────────────────────────────┘
```

**三层职责:**

| 层      | 职责                        | 不做什么               |
| ------- | --------------------------- | ---------------------- |
| `core/` | 纯计算,无 UI,无 DB        | 不写数据库,不渲染界面 |
| `db/`   | 纯数据库操作                | 不做计算,不渲染界面   |
| `ui/`   | 纯界面逻辑,调用 core 和 db | 不做底层计算           |

---

## 4. 目录结构

```
项目根目录/

├── app.py                    # 主入口:初始化DB,组装6个Tab
├── requirements.txt          # 依赖清单

├── core/                     # 计算引擎(纯Python,无副作用)
│   ├── __init__.py           # 空文件
│   ├── config.py             # 全局开关(DEBUG=True/False)
│   ├── debug.py              # 调试输出工具(受config.DEBUG控制)
│   ├── fetcher.py            # HTTP Range Request 读取远程权重
│   ├── layer_profile.py      # 自动推断模型层结构
│   ├── metrics.py            # 计算王氏五定律全部指标
│   ├── plotter.py            # matplotlib 静态图(4×3,18×20in,300dpi)
│   ├── plotter_plotly.py     # Plotly 原生交互图(12×1,全宽)
│   └── table_gen.py          # 论文表格生成(6张表,LaTeX/Markdown/CSV)

├── db/                       # 数据持久化层
│   ├── __init__.py           # 空文件
│   ├── schema.py             # 建表SQL + 数据库连接
│   ├── writer.py             # 写入分析结果 + 断点续传 + 级联删除
│   └── reader.py             # 查询排行榜、模型详情、原始数据

└── ui/                       # Gradio 界面层
    ├── __init__.py           # 空文件
    ├── tab_inspect.py        # Tab1:模型结构探测
    ├── tab_analyze.py        # Tab2:分析模型 + 写库
    ├── tab_leaderboard.py    # Tab3:王氏评分排行榜
    ├── tab_database.py       # Tab4:数据库浏览 + 模型删除
    ├── tab_plot.py           # Tab5:作图(Plotly交互 + matplotlib导出)
    └── tab_tables.py         # Tab6:论文表格生成
```

---

## 5. 各层详细说明

### 5.1 `core/` 层——计算引擎

#### `core/config.py`

```python
DEBUG = False   # True → 打印详细调试信息;False → 静默运行
```

全局唯一开关。所有调试输出都受这个控制。

---

#### `core/debug.py`

| 函数     | 签名                           | 用途                                   |
| -------- | ------------------------------ | -------------------------------------- |
| `dlog`   | `(lines: list[str], msg: str)` | 向日志列表追加调试信息(仅DEBUG=True) |
| `dprint` | `(msg: str)`                   | 打印到stdout(仅DEBUG=True)           |

`dlog` 用于 `metrics.py``tab_analyze.py`(有 lines 列表的地方)。
`dprint` 用于 `fetcher.py`(没有 lines 列表的地方)。

---

#### `core/fetcher.py`

**核心思想**:safetensors 文件头记录了每个 tensor 的字节偏移,
用 HTTP Range Request 只下载需要的字节,无需下载整个文件。

| 函数                      | 签名                                             | 返回                             | 用途                       |
| ------------------------- | ------------------------------------------------ | -------------------------------- | -------------------------- |
| `get_file_url`            | `(model_id, filename)`                           | `str`                            | 拼接 HF 下载 URL           |
| `read_safetensors_header` | `(url, token)`                                   | `(header_dict, header_size)`     | 读取文件头(两次HTTP请求) |
| `load_tensor_remote`      | `(url, tensor_name, header, header_size, token)` | `torch.Tensor`                   | 按名读取单个tensor         |
| `get_safetensor_files`    | `(model_id, token)`                              | `list[str]`                      | 列出所有.safetensors文件   |
| `find_index_file`         | `(model_id, token)`                              | `dict\|None`                     | 读取分片索引文件           |
| `get_all_shard_files`     | `(model_id, token)`                              | `list[str]`                      | 获取全部分片文件名         |
| `load_all_shard_headers`  | `(model_id, token)`                              | `dict[filename, (header, size)]` | 读取所有分片的header       |
| `check_quantization`      | `(model_id, token)`                              | `(is_blocked, message)`          | 三重量化检测               |
| `http_error_msg`          | `(e, model_id)`                                  | `str`                            | HTTP错误码转中文提示       |

**`load_all_shard_headers` 返回结构:**
```python
{
    "model-00001-of-00006.safetensors": (header_dict, header_size),
    "model-00002-of-00006.safetensors": (header_dict, header_size),
    ...
}
# header_dict 结构:
{
    "model.layers.0.self_attn.q_proj.weight": {
        "dtype": "BF16",
        "shape": [4096, 4096],
        "data_offsets": [0, 33554432]
    },
    ...
}
```

**量化检测四重逻辑(按顺序):**
1. 检测 `config.json` 中的 `quantization_config` 字段
2. 检测模型名是否含 `gptq/awq/gguf` 关键词
3. 检测文件列表是否有 `.gguf` 文件
4. 检测 header 中是否有量化专用 key(如 `qweight`, `qzeros`)

---

#### `core/layer_profile.py`

**核心思想**:从权重文件的 key 名自动推断模型结构,零 hard coding,
不依赖模型名称或配置文件(配置文件只是辅助参考)。

**关键数据结构:**

```python
@dataclass
class QKVKey:
    shard: str    # 所在分片文件名,如 "model-00001-of-00006.safetensors"
    key:   str    # 完整tensor名,如 "model.layers.0.self_attn.q_proj.weight"
    shape: list   # tensor形状,如 [4096, 4096]

@dataclass
class LayerProfile:
    prefix:    str         # 组件前缀,如 "model.language_model."
    layer_idx: int         # 层号(原始safetensors key中的N)
    q:         QKVKey      # Q权重位置
    k:         QKVKey      # K权重位置
    v:         QKVKey|None # V权重位置(None表示K=V共享)
    head_dim:    int       # 每个head的维度
    n_q_heads:   int       # Q head数量
    n_kv_heads:  int       # KV head数量(GQA时 < n_q_heads)
    d_model:     int       # 模型隐层维度(= q_shape[1])
    kv_shared:   bool      # True = K和V共享(如Gemma全局层)
    complete:    bool      # True = Q/K都存在且head_dim推断成功
    infer_ok:   bool       # head_dim推断是否成功
    head_dim_source: str   # 推断来源:"k_norm"/"q_norm"/"config"/"enum"
```

| 函数                    | 签名                                 | 返回                                 | 用途                                                |
| ----------------------- | ------------------------------------ | ------------------------------------ | --------------------------------------------------- |
| `classify_qkv_suffix`   | `(suffix: str)`                      | `'q'/'k'/'v'/None`                   | 从key后缀判断是Q/K/V                                |
| `is_norm_key`           | `(suffix: str)`                      | `bool`                               | 判断是否为norm key(辅助推断head_dim)              |
| `scan_model_structure`  | `(all_shard_headers, config_params)` | `dict[(prefix,layer), LayerProfile]` | **核心函数**:扫描全部headers,构建LayerProfile字典 |
| `summarize_structure`   | `(profiles)`                         | `str`                                | 生成人类可读的结构报告(Tab1使用)                  |
| `extract_config_params` | `(config: dict)`                     | `dict`                               | 从config.json提取关键参数(兼容Gemma4嵌套结构)     |

**`scan_model_structure` 工作流程:**
```
第一遍扫描:遍历所有shard的所有key
  → 用正则 r'layers\.(\d+)\.' 提取层号
  → prefix = key的layers.N.之前部分
  → suffix = key的layers.N.之后部分
  → classify_qkv_suffix(suffix) → 归类为Q/K/V
  → is_norm_key(suffix) → 收集k_norm/q_norm形状(辅助推断head_dim)

第二遍构建:对每个(prefix, layer_idx)槽
  → 检查Q/K是否都存在(必要条件)
  → V不存在 → kv_shared=True
_infer_head_dim() → 推断head_dim(5个优先级)
  → 计算n_q_heads, n_kv_heads, d_model
  → 构建LayerProfile
```

**head_dim 推断优先级:**
```
1. k_norm.shape[0]        ← 最可靠(Gemma系列有这个)
2. q_norm.shape[0]        ← 备用
3. config["head_dim"]     ← config.json直接给出
4. config["hidden_size"] / config["num_attention_heads"]  ← 计算得出
5. 枚举候选值 [512,256,128,96,80,64,48,40,32,16]  ← 最后手段
```

**KV共享检测(Gemma全局层):**
```
V的key不存在于任何shard header → kv_shared=True → layer_type="global"
```

---

#### `core/metrics.py`

对一层的Q/K/V权重矩阵计算所有指标。

**底层计算函数:**

| 函数          | 签名                   | 返回                | 对应定律               |
| ------------- | ---------------------- | ------------------- | ---------------------- |
| `pearson`     | `(a, b: Tensor)`       | `float`             | 第一定律               |
| `spearman_r`  | `(a, b: Tensor)`       | `float`             | 第一定律(补充)       |
| `ssr`         | `(a, b: Tensor)`       | `float`             | 第二定律               |
| `svr`         | `(a, b: Tensor)`       | `(alpha, residual)` | 尺度因子               |
| `cos_U`       | `(U_a, U_b: Tensor)`   | `float`             | 第四定律(左奇异向量) |
| `cos_V`       | `(Vt_a, Vt_b: Tensor)` | `float`             | 第五定律(右奇异向量) |
| `sigma_stats` | `(s: Tensor)`          | `(max, min, cond)`  | 第三定律               |

**`ssr` 计算细节:**
```python
# 归一化后逐元素绝对差的均值
n  = min(len(a), len(b))
an = a[:n] / ||a[:n]||    # L2归一化
bn = b[:n] / ||b[:n]||
SSR = mean(|an - bn|)
```

**主分析函数:**

```python
def analyze_layer(
    W_q: torch.Tensor,      # shape: [n_q_heads * head_dim, d_model]
    W_k: torch.Tensor,      # shape: [n_kv_heads * head_dim, d_model]
    W_v: torch.Tensor,      # shape: [n_kv_heads * head_dim, d_model]
    profile: LayerProfile,
) -> tuple[list[dict], str]:
    # 返回:(records列表, 格式化日志字符串)
```

**`analyze_layer` 工作流程:**
```
对每个 kv_head(0 ~ n_kv_heads-1):
  切片:k_t = W_k[kv_h*d_head : (kv_h+1)*d_head, :]
  SVD:U_k, s_k, Vt_k = svd(k_t)
  计算 sigma_stats(s_k)

  对每个 q_head(属于这个kv_head的group,GQA时 group = n_q/n_kv):
    切片:q_t = W_q[h*d_head : (h+1)*d_head, :]
    SVD:U_q, s_q, Vt_q = svd(q_t)

    计算所有指标:
      pearson_QK, spearman_QK, ssr_QK, alpha_QK  ← Q vs K奇异值
      pearson_QV, ssr_QV, alpha_QV                ← Q vs V奇异值
      ssr_KV, alpha_KV                            ← K vs V奇异值
      cosU_QK, cosU_QV, cosU_KV                  ← 左奇异向量
      cosV_QK, cosV_QV, cosV_KV                  ← 右奇异向量
      sigma_max/min/cond for Q, K, V

    append到records

特殊处理:kv_shared=True时,KV指标设为理论值(ssr=0, pearson=1, cosU=1等)
```

**`records` 每条记录的字段(共37个字段):**
```python
{
    "prefix": str,       "layer": int,
    "kv_head": int,      "q_head": int,
    "kv_shared": bool,   "head_dim": int,
    "d_model": int,      "n_q_heads": int,    "n_kv_heads": int,
    # 第一定律
    "pearson_QK": float, "spearman_QK": float,
    "pearson_QV": float, "pearson_KV": float,
    # 第二定律
    "ssr_QK": float,     "ssr_QV": float,     "ssr_KV": float,
    # 第三定律
    "sigma_max_Q": float, "sigma_min_Q": float, "cond_Q": float,
    "sigma_max_K": float, "sigma_min_K": float, "cond_K": float,
    "sigma_max_V": float, "sigma_min_V": float, "cond_V": float,
    # 第四定律
    "cosU_QK": float,    "cosU_QV": float,    "cosU_KV": float,
    # 第五定律
    "cosV_QK": float,    "cosV_QV": float,    "cosV_KV": float,
    # 尺度因子
    "alpha_QK": float,   "alpha_QV": float,   "alpha_KV": float,
    "alpha_res_QK": float, "alpha_res_QV": float, "alpha_res_KV": float,
}
```

```python
def summarize_records(records: list[dict], model_id: str) -> str:
    # 对records做统计汇总,返回格式化文本
    # 按prefix分组,对每个指标计算 Median/Mean/Min/Max
    # KV指标自动排除kv_shared=True的行(避免理论值污染统计)
```

---

### 5.2 `db/` 层——数据持久化

#### `db/schema.py`

数据库路径逻辑:
```python
def get_db_path() -> str:
    if os.path.exists("/data"):   # HF Space bucket挂载点
        return "/data/wang_laws.db"
    return "wang_laws.db"         # 本地开发回退
```

| 函数             | 签名     | 用途                               |
| ---------------- | -------- | ---------------------------------- |
| `get_db_path`    | `()`     | 返回数据库文件路径                 |
| `get_connection` | `()`     | 返回SQLite连接(WAL模式,Row工厂) |
| `init_db`        | `()`     | 建表+建索引,幂等,返回连接        |
| `get_db_stats`   | `(conn)` | 返回各表行数+文件大小              |

---

#### `db/writer.py`

| 函数                     | 签名                                                | 用途                                                                      |
| ------------------------ | --------------------------------------------------- | ------------------------------------------------------------------------- |
| `infer_layer_type`       | `(kv_shared: bool)`                                 | `True→"global"`, `False→"standard"`                                       |
| `infer_modality`         | `(prefix: str)`                                     | 从 prefix 推断模态:language/vision/audio                                 |
| `check_write_permission` | `(admin_token: str)`                                | 验证 WRITE_TOKEN,返回 bool                                               |
| `get_analyzed_layers`    | `(conn, model_id, prefix)`                          | 返回已完成的层号集合(断点续传用)                                        |
| `is_layer_complete`      | `(conn, model_id, prefix, layer, expected_records)` | 检查某层记录数是否达到预期                                                |
| `upsert_model`           | `(conn, model_id, model_type, notes)`               | 写入/更新模型元数据                                                       |
| `upsert_component`       | `(conn, model_id, prefix, n_layers, ...)`           | 写入/更新组件信息                                                         |
| `write_layer_records`    | `(conn, model_id, records: list[dict])`             | 批量写入一层的逐头数据(INSERT OR REPLACE)                               |
| `_pseudobulk_col`        | `(rows, col_name: str)`                             | Pseudo-bulk 两步聚合:消除 GQA 伪重复计数                                 |
| `_calc_summary_row`      | `(rows, model_id, prefix, layer_type)`              | 用 pseudo-bulk 计算单行汇总统计                                           |
| `update_model_summary`   | `(conn, model_id, prefix)`                          | 重算并写入 model_summary 的 all/standard/global 三行                      |
| `refresh_all_summaries`  | `(conn)`                                            | 遍历所有(model_id, prefix)重跑 update_model_summary,供 Tab3 Refresh 调用 |
| `delete_model`           | `(conn, model_id, admin_token)`                     | 级联删除模型所有数据,需 WRITE_TOKEN 验证                                 |

**`update_model_summary` 逻辑:**
```
对 layer_type in ["all", "standard", "global"]:
  从 layer_head_metrics 查对应行
  用 _pseudobulk_col() 两步聚合(先按 kv_head 组内 median,再跨组 median)
    → 消除 GQA 模型(如 LLaMA-3 32Q/8KV)的伪重复计数偏差
  wang_score 统一用 standard 层的 pseudo-bulk median(ssr_QK) 计算
    (即使写 all/global 行,wang_score 也来自 standard 层)
  INSERT OR REPLACE 写入 model_summary
```

**`delete_model` 逻辑:**
```
1. check_write_permission(admin_token) → 失败直接返回错误
2. 查 models 表确认模型存在
3. 统计各子表行数(用于返回日志)
4. 按顺序级联删除:
   layer_head_metrics → model_summary → components → models
5. 返回 (True, 详细删除日志)
```

---

#### `db/reader.py`

| 函数                  | 签名                                                           | 返回           | 用途                         |
| --------------------- | -------------------------------------------------------------- | -------------- | ---------------------------- |
| `get_leaderboard`     | `(conn, prefix_filter, layer_type, limit)`                     | `pd.DataFrame` | 排行榜查询,按wang_score降序 |
| `get_model_summary`   | `(conn, model_id)`                                             | `pd.DataFrame` | 某模型所有组件的汇总统计     |
| `get_layer_metrics`   | `(conn, model_id, prefix, layer_type, start_layer, end_layer)` | `pd.DataFrame` | 逐头原始数据查询             |
| `get_analyzed_models` | `(conn)`                                                       | `pd.DataFrame` | 所有已分析模型列表           |
| `get_resume_status`   | `(conn, model_id, prefix)`                                     | `dict`         | 断点续传状态:已完成层号集合 |

---

### 5.3 `ui/` 层——用户界面

#### `ui/tab_inspect.py` — Tab1:结构探测

**函数:**

```python
def inspect_model(model_id, hf_token, progress) -> (str, pd.DataFrame):
    """
    工作流程:
    1. check_quantization()         ← 量化检测,失败则返回
    2. 读取 config.json             ← extract_config_params()
    3. load_all_shard_headers()     ← 读取所有分片header
    4. scan_model_structure()       ← 构建LayerProfile字典
    5. summarize_structure()        ← 生成文本报告
    6. 构建概览DataFrame            ← 每层一行
    返回:(日志文本, 层结构DataFrame)
    """

def build_tab_inspect() -> (inspect_model_id, inspect_token):
    """
    构建Tab1的Gradio组件
    返回:(model_id文本框, token文本框)
    ← 返回值供app.py做Tab1→Tab2的联动同步
    """
```

**UI组件:**
```
模型ID输入框 + Token输入框 + 探测按钮
→ 日志文本框(结构报告)
→ 层结构表格(prefix/layer/d_model/head_dim/n_q/n_kv/kv_shared等)
```

---

#### `ui/tab_analyze.py` — Tab2:分析(核心Tab)

**函数:**

```python
def run_analysis(model_id, hf_token, start_layer, end_layer, admin_token, progress)
    -> (str, pd.DataFrame):
    """
    完整工作流程:

    [准备阶段]
    1.  init_db()                          ← 获取DB连接
    2.  check_quantization()               ← 量化检测
    3.  读取 config.json
    4.  load_all_shard_headers()           ← 读所有分片header
        (404/网络错误 → 提前返回,DB零污染)
    5.  scan_model_structure()             ← 构建LayerProfile字典
    6.  upsert_model()                     ← 写模型元数据到DB
        (注意:故意在 shard headers 加载成功后才写,
          防止模型名拼写错误产生脏数据)
    7.  upsert_component() for each prefix ← 写组件信息到DB

    [断点续传检查]
    8.  get_analyzed_layers() for each prefix
        → done_layers: dict[prefix, set[int]]
        → 打印待分析层和已跳过层

    [逐层分析循环]
    for each (prefix, layer_idx) in filtered(按prefix+layer排序):
      9.  检查:layer_idx in done_layers[prefix] → continue(跳过)
      10. load_tensor_remote(Q)              ← HTTP Range Request
      11. load_tensor_remote(K)
      12. kv_shared ? W_v=W_k.clone() : load_tensor_remote(V)
      13. analyze_layer(W_q, W_k, W_v, prof) ← 计算五定律
      14. write_layer_records(conn, model_id, records) ← 写DB
      15. update_model_summary(conn, model_id, prefix) ← 更新排行榜
      16. del W_q, W_k, W_v                 ← 释放内存

    [收尾]
    17. 更新 models.analyze_sec(总耗时)
    18. summarize_records()                  ← 生成汇总文本
    返回:(日志文本, 逐头结果DataFrame)
    """

def build_tab_analyze() -> (model_id_input, token_input):
    """构建Tab2的Gradio组件,返回值供app.py联动"""
```

**UI组件:**
```
模型ID + Token + 起始层号 + 结束层号 + Admin Write Token + 分析按钮
侧边栏:推荐模型列表 + 层号说明 + Reviewer Note(留空token可只读分析)
→ 分析日志文本框(逐头详情)
→ 逐头结果表格(37列全指标)
```

---

#### `ui/tab_leaderboard.py` — Tab3:排行榜

**函数:**

```python
def _format_leaderboard(df: pd.DataFrame) -> pd.DataFrame:
    """
    格式化显示:
    - model_id → model_name(取最后一段)
    - wang_score → wang_score_pct(百分制字符串)
    - 数值列 → 6位小数字符串
    - 选择展示列(隐藏冗余列)
    """

def load_leaderboard(modality, layer_type) -> (pd.DataFrame, str):
    """
    调用 refresh_all_summaries(conn) 静默重算所有模型汇总
      → 自动将历史数据迁移到 pseudo-bulk 聚合
    调用 reader.get_leaderboard()
    modality 控制按模态过滤(language/vision/audio/all)
    layer_type="all" → 实际查 "standard"(排行榜默认用standard)
    """

def build_tab_leaderboard():
    """
    UI:组件过滤输入框 + 层类型下拉 + 刷新按钮
    → 状态文本 + 排行榜表格 + 指标说明
    用户手动点刷新(不自动加载)
    """
```

---

#### `ui/tab_database.py` — Tab4:数据库浏览

**函数:**

```python
def load_db_stats() -> str:
    """调用 get_db_stats(),返回各表行数+文件大小"""

def load_model_list() -> pd.DataFrame:
    """调用 get_analyzed_models(),返回模型列表"""

def load_model_detail(model_id) -> (pd.DataFrame, str):
    """
    调用 get_model_summary() → summary_df
    调用 get_resume_status() for each prefix → 断点续传状态文本
    """

def run_delete_model(model_id, admin_token) -> (str, pd.DataFrame):
    """
    调用 db/writer.delete_model() 执行级联删除
    需要 Admin Write Token 验证
    删除成功后自动刷新 models_table
    返回 (状态文本, 刷新后的模型列表DataFrame)
    """

def load_layer_data(model_id, prefix, layer_type, start_layer, end_layer)
    -> (pd.DataFrame, str):
    """调用 get_layer_metrics(),返回逐头原始数据"""

def build_tab_database():
    """
    UI分为5个区块:
    1. 数据库统计(行数+文件大小)
    2. 已分析模型列表
    3. 🗑️ 删除模型(Model ID + Admin Token + Delete按钮,variant="stop"红色警示)
       → 删除成功后自动刷新模型列表
    4. 模型详情+断点续传状态
    5. 逐头原始数据查询(支持按modality/layer_type/层号范围过滤)
    """
```

---

#### `ui/tab_plot.py` — Tab5:作图

**两条独立渲染路径(无嵌套 gr.Tabs(),用两个并排按钮区分):**

| 按钮          | 引擎                     | 速度 | 输出                         |
| ------------- | ------------------------ | ---- | ---------------------------- |
| ⚡ Interactive | `core/plotter_plotly.py` | ~2s  | 浏览器内交互,hover/zoom     |
| 🖨️ Export      | `core/plotter.py`        | ~30s | PNG(300dpi) + PDF + SVG 下载 |

**函数:**

```python
def gen_single_plotly(model_id, modality, start_l, end_l, show_band) -> (go.Figure, str):
    """从DB加载数据,调用 plotly_single(),返回 Plotly Figure"""

def gen_single_export(model_id, modality, start_l, end_l, show_band) -> (str, img, png, pdf, svg, zip):
    """从DB加载数据,调用 plot_single_model(),保存 PNG/PDF/SVG,返回下载链接"""

def gen_compare_plotly(model_a, model_b, modality, start_l, end_l, show_band, show_delta) -> (go.Figure, str):
    """双模型对比,调用 plotly_compare()"""

def gen_compare_export(model_a, model_b, modality, start_l, end_l, show_band, show_delta) -> (str, img, png, pdf, svg, zip):
    """双模型对比导出,调用 plot_compare_models()"""

def build_tab_plot():
    """
    UI分为两个 Accordion:
    1. 📊 Single Model:选模型 → ⚡Interactive / 🖨️Export
    2. 📊 Two-Model Comparison:选A+B → ⚡Interactive / 🖨️Export + Δ填充开关
    共享控件:Modality / Start Layer / End Layer / IQR band 开关
    """
```

**12子图布局(Plotly 12×1 全宽):**
```
行 0:pearson_QK       定律1 谱线性对齐
行 1:ssr_QK           定律2 谱形状保真度
行 2:alpha_QK         定律1+2 尺度因子α
行 3:sigma_max_Q      定律3 最大奇异值(Q)
行 4:sigma_max_K      定律3 最大奇异值(K)
行 5:cond_Q + cond_K  定律3 条件数κ(双线,对数坐标)
行 6:cosU_QK          定律4 输出子空间 Q-K
行 7:cosU_QV          定律4 输出子空间 Q-V(超正交)
行 8:cosU_KV          定律4 输出子空间 K-V(超正交)
行 9:cosV_QK          定律5 输入子空间 Q-K
行10:cosV_QV          定律5 输入子空间 Q-V
行11:cosV_KV          定律5 输入子空间 K-V
```

---

#### `ui/tab_tables.py` — Tab6:论文表格

**一键生成6张论文表格,数据来源:language modality + standard layers only。**

**函数:**

```python
def generate_tables(selected_models, table2_model_a, table2_model_b, group_text)
    -> (status, t1~t6 DataFrames, latex_str, md_str, csv×6, latex_file, md_file, zip):
    """
    工作流程:
    1. _load_all_models(selected_models) ← 从DB读取所有选中模型数据
    2. _parse_groups(group_text)         ← 解析用户定义的层组(如"0-11,12-23")
    3. generate_all_tables()             ← core/table_gen.py 生成6张表
    4. format_all_latex() / format_all_markdown() ← 格式化输出
    5. 保存 CSV × 6 + .tex + .md → 打包 ZIP
    返回:所有输出供 Gradio 组件展示和下载
    """

def build_tab_tables():
    """
    UI分为:
    - 模型多选框(CheckboxGroup)+ Refresh按钮
    - Table2专用:Model A / Model B 下拉 + 层组输入框
    - 🚀 Generate All Tables 按钮
    - 6个 Accordion,每个内含 DataFrame + CSV下载
    - LaTeX / Markdown 代码框(可直接复制粘贴)
    - 批量下载:.tex / .md / ZIP
    """
```

**6张表说明:**

| 表格    | 内容                                       | 对应定律  |
| ------- | ------------------------------------------ | --------- |
| Table 1 | 跨模型汇总:Pearson r, SSR                 | 定律1 & 2 |
| Table 2 | SSR 层组趋势(RL改善效果,用户自定义层组) | 定律2     |
| Table 3 | 输出子空间 cosU:Q-K, Q-V, K-V + 随机基线  | 定律4     |
| Table 4 | 输入子空间 cosV:Q-K, Q-V, K-V + 随机基线  | 定律5     |
| Table 5 | 条件数κ:全层/第0层/深层 分别统计          | 定律3     |
| Table 6 | Wang Score 排行榜(按分降序)              | 定律1 & 2 |

---

### 5.4 `app.py`——主入口

```python
# 启动时执行(模块级)
init_db()   # 建表,幂等

# Gradio Blocks
with gr.Blocks(...) as demo:
    # 标题 + 五定律表格(英中双语并排)+ DOI徽章

    with gr.Tabs():
        inspect_model_id, inspect_token = build_tab_inspect()
        analyze_model_id, analyze_token = build_tab_analyze()
        build_tab_leaderboard()
        build_tab_database()
        build_tab_plot()
        build_tab_tables()

    # Tab1 → Tab2 联动(避免重复输入)
    inspect_model_id.change(fn=lambda x:x,
        inputs=inspect_model_id, outputs=analyze_model_id)
    inspect_token.change(fn=lambda x:x,
        inputs=inspect_token, outputs=analyze_token)
```

---

## 6. 数据库表结构

共4张表,SQLite 存储于 `/data/wang_laws.db`### `models` — 模型基本信息
```sql
CREATE TABLE models (
    model_id      TEXT PRIMARY KEY,   -- "google/gemma-4-e2b"
    model_type    TEXT,               -- "gemma4" / "qwen2" 等
    analyzed_at   TIMESTAMP,          -- 最后分析时间
    analyze_sec   REAL,               -- 本次分析总耗时(秒)
    notes         TEXT                -- 备注
);
```

### `components` — 组件信息
```sql
CREATE TABLE components (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    model_id      TEXT NOT NULL,
    prefix        TEXT NOT NULL,      -- "model.language_model."
    n_layers      INTEGER,            -- 该组件完整层数
    head_dim_min  INTEGER,            -- 最小head_dim(异构层存在时有意义)
    head_dim_max  INTEGER,            -- 最大head_dim
    has_kv_shared INTEGER DEFAULT 0,  -- 是否有K=V共享层
    has_global    INTEGER DEFAULT 0,  -- 是否有global层
    d_model       INTEGER,            -- 输入维度
    UNIQUE(model_id, prefix)
);
```

### `layer_head_metrics` — 逐头原始数据(主数据表)
```sql
CREATE TABLE layer_head_metrics (
    id            INTEGER PRIMARY KEY AUTOINCREMENT,
    model_id      TEXT NOT NULL,
    prefix        TEXT NOT NULL,
    layer         INTEGER NOT NULL,
    layer_type    TEXT DEFAULT 'standard',  -- "standard" / "global"
    kv_head       INTEGER NOT NULL,
    q_head        INTEGER NOT NULL,
    kv_shared     INTEGER DEFAULT 0,   -- 1=K=V共享(理论值),0=正常
    head_dim      INTEGER,
    d_model       INTEGER,
    n_q_heads     INTEGER,
    n_kv_heads    INTEGER,
    -- 第一定律
    pearson_QK REAL, spearman_QK REAL, pearson_QV REAL, pearson_KV REAL,
    -- 第二定律
    ssr_QK REAL, ssr_QV REAL, ssr_KV REAL,
    -- 第三定律
    sigma_max_Q REAL, sigma_min_Q REAL, cond_Q REAL,
    sigma_max_K REAL, sigma_min_K REAL, cond_K REAL,
    sigma_max_V REAL, sigma_min_V REAL, cond_V REAL,
    -- 第四定律
    cosU_QK REAL, cosU_QV REAL, cosU_KV REAL,
    -- 第五定律
    cosV_QK REAL, cosV_QV REAL, cosV_KV REAL,
    -- 尺度因子
    alpha_QK REAL, alpha_res_QK REAL,
    alpha_QV REAL, alpha_res_QV REAL,
    alpha_KV REAL, alpha_res_KV REAL,

    UNIQUE(model_id, prefix, layer, kv_head, q_head)
);
```

### `model_summary` — 汇总统计(排行榜用)
```sql
CREATE TABLE model_summary (
    model_id          TEXT NOT NULL,
    prefix            TEXT NOT NULL,
    layer_type        TEXT NOT NULL DEFAULT 'all',  -- all/standard/global
    -- 第一定律
    median_pearson_QK REAL, mean_pearson_QK REAL,
    -- 第二定律
    median_ssr_QK REAL, mean_ssr_QK REAL,
    median_ssr_QV REAL, mean_ssr_QV REAL,
    -- 第三定律
    median_cond_Q REAL, mean_cond_Q REAL,
    -- 第四定律
    median_cosU_QK REAL, median_cosU_QV REAL,
    -- 第五定律
    median_cosV_QK REAL, median_cosV_QV REAL,
    -- 王氏评分(始终用standard层计算,即使layer_type=all/global)
    wang_score        REAL,
    n_layers          INTEGER,
    n_records         INTEGER,
    updated_at        TIMESTAMP,

    PRIMARY KEY(model_id, prefix, layer_type)
);
```

**每个(model_id, prefix)在model_summary中有3行:**
```
(model_id, prefix, "all")       ← 全部层混合统计
(model_id, prefix, "standard")  ← 只含standard层
(model_id, prefix, "global")    ← 只含global层(如Gemma全局层)
```

**layer_type 推断规则(零hard coding):**
```
kv_shared=True  → layer_type="global"
kv_shared=False → layer_type="standard"
```

---

## 7. 数据流全链路

```
用户输入模型ID(如 "google/gemma-4-e2b")


[Tab1 或 Tab2]
check_quantization()
    → 检测config.json / 模型名 / 文件列表 / header内容
    → 量化模型直接拒绝


load_all_shard_headers()
    → 对每个.safetensors文件:
        HTTP GET bytes=0-7          → header_size(8字节小端整数)
        HTTP GET bytes=8-{8+size}   → JSON header
    → 返回 {filename: (header_dict, header_size)}


scan_model_structure()
    → 两遍扫描所有key → 构建 {(prefix,layer): LayerProfile}
    → 自动推断:head_dim / n_q_heads / n_kv_heads / kv_shared

    ▼(Tab2专有)
断点续传检查
    → get_analyzed_layers() → done_layers: dict[prefix, set[int]]

    ▼(逐层循环)
load_tensor_remote(W_q)  → HTTP GET bytes={abs_start}-{abs_end}
load_tensor_remote(W_k)  → 同上
load_tensor_remote(W_v)  → 同上(kv_shared时直接clone W_k)


analyze_layer(W_q, W_k, W_v, profile)
    → 按head切片
    → SVD分解每个head
    → 计算37个指标
    → 返回 records: list[dict]


write_layer_records(conn, model_id, records)
    → INSERT OR REPLACE 批量写入 layer_head_metrics


update_model_summary(conn, model_id, prefix)
    → 查询 layer_head_metrics
    → 计算 median/mean
    → wang_score = 1 - median(ssr_QK) [用standard层]
    → INSERT OR REPLACE 写入 model_summary(all/standard/global 3行)


[Tab3 排行榜]
get_leaderboard()
    → SELECT from model_summary WHERE layer_type='standard'
    → ORDER BY wang_score DESC
    → 格式化展示
```

---

## 8. 函数调用关系图

```
app.py
├── init_db()                              [db/schema.py]
├── build_tab_inspect()                    [ui/tab_inspect.py]
│   └── inspect_model()
│       ├── check_quantization()           [core/fetcher.py]
│       ├── extract_config_params()        [core/layer_profile.py]
│       ├── load_all_shard_headers()       [core/fetcher.py]
│       │   ├── get_all_shard_files()
│       │   │   └── find_index_file()
│       │   └── read_safetensors_header()
│       ├── scan_model_structure()         [core/layer_profile.py]
│       │   ├── classify_qkv_suffix()
│       │   ├── is_norm_key()
│       │   └── _infer_head_dim()
│       └── summarize_structure()

├── build_tab_analyze()                    [ui/tab_analyze.py]
│   └── run_analysis()
│       ├── init_db()                      [db/schema.py]
│       ├── check_quantization()           [core/fetcher.py]
│       ├── extract_config_params()        [core/layer_profile.py]
│       ├── load_all_shard_headers()       [core/fetcher.py]  ← 成功后才写DB
│       ├── scan_model_structure()         [core/layer_profile.py]
│       ├── upsert_model()                 [db/writer.py]     ← 在此之后写入
│       ├── upsert_component()             [db/writer.py]
│       ├── get_analyzed_layers()          [db/writer.py]
│       ├── load_tensor_remote() ×3        [core/fetcher.py]
│       ├── analyze_layer()                [core/metrics.py]
│       │   ├── pearson()
│       │   ├── spearman_r()
│       │   ├── ssr()
│       │   ├── svr()
│       │   ├── cos_U()
│       │   ├── cos_V()
│       │   └── sigma_stats()
│       ├── write_layer_records()          [db/writer.py]
│       │   └── infer_layer_type()
│       ├── update_model_summary()         [db/writer.py]
│       │   ├── _pseudobulk_col()
│       │   └── _calc_summary_row()
│       └── summarize_records()            [core/metrics.py]

├── build_tab_leaderboard()                [ui/tab_leaderboard.py]
│   └── load_leaderboard()
│       ├── init_db()                      [db/schema.py]
│       ├── refresh_all_summaries()        [db/writer.py]
│       │   └── update_model_summary() ×N
│       ├── get_leaderboard()              [db/reader.py]
│       └── _format_leaderboard()

├── build_tab_database()                   [ui/tab_database.py]
│   ├── load_db_stats()
│   │   └── get_db_stats()                 [db/schema.py]
│   ├── load_model_list()
│   │   └── get_analyzed_models()          [db/reader.py]
│   ├── run_delete_model()
│   │   ├── delete_model()                 [db/writer.py]
│   │   │   └── check_write_permission()
│   │   └── load_model_list()              ← 删除后自动刷新
│   ├── load_model_detail()
│   │   ├── get_model_summary()            [db/reader.py]
│   │   └── get_resume_status()            [db/reader.py]
│   └── load_layer_data()
│       └── get_layer_metrics()            [db/reader.py]

├── build_tab_plot()                       [ui/tab_plot.py]
│   ├── gen_single_plotly()
│   │   ├── get_layer_metrics()            [db/reader.py]
│   │   └── plotly_single()               [core/plotter_plotly.py]
│   ├── gen_single_export()
│   │   ├── get_layer_metrics()            [db/reader.py]
│   │   ├── plot_single_model()            [core/plotter.py]
│   │   └── save_figure()
│   ├── gen_compare_plotly()
│   │   ├── get_layer_metrics() ×2         [db/reader.py]
│   │   └── plotly_compare()              [core/plotter_plotly.py]
│   └── gen_compare_export()
│       ├── get_layer_metrics() ×2         [db/reader.py]
│       ├── plot_compare_models()          [core/plotter.py]
│       └── save_figure()

└── build_tab_tables()                     [ui/tab_tables.py]
    └── generate_tables()
        ├── get_layer_metrics() ×N         [db/reader.py]
        ├── generate_all_tables()          [core/table_gen.py]
        │   ├── make_table1()
        │   ├── make_table2()
        │   ├── make_table3()
        │   ├── make_table4()
        │   ├── make_table5()
        │   └── make_table6()
        ├── format_all_latex()
        └── format_all_markdown()
```

---

## 9. 关键设计决策

### 零 hard coding 原则
任何模型相关的参数(head_dim、层数、组件结构)
都从权重文件的 key 名自动推断,不写死任何模型名或层号。

### GQA 支持
当 `n_q_heads > n_kv_heads` 时(如 Llama-3-8B 的 32Q/8KV),
`group = n_q / n_kv`,每个KV head对应group个Q head,
全部独立计算,每个Q head一条记录。

### K=V 共享(Gemma全局层)
Gemma-4-31B 每6层有一个全局层,V权重不存在(K和V共享)。
检测方式:V的key不在任何shard的header中。
处理方式:`W_v = W_k.clone()`,KV相关指标设为理论值。
存储方式:`kv_shared=1`,`layer_type="global"`。

### 断点续传粒度
以 `(model_id, prefix, layer)` 为粒度。
某层的所有head全部写入才算完成。
允许随时中断,下次从未完成的层继续。

### 排行榜的 wang_score
无论 `model_summary` 的 `layer_type` 是 all/standard/global,
`wang_score` 统一从 standard 层的 `ssr_QK` 计算,
避免全局层(K=V共享,SSR=0)人为拉高评分。

### 每个(model_id, prefix)在排行榜中是一行
排行榜以 `(model_id, prefix)` 为单位,
多模态模型(如Gemma-4)的language_model和vision_tower分别占一行。

### 防脏数据写入(Lazy Write)
`upsert_model()` 和 `upsert_component()` 故意推迟到 `load_all_shard_headers()` 成功之后才调用。
模型名拼写错误(如 "Meta-Llama-3-70B-intruct" 少一个s)会在 HF 返回 404 时提前 return,
DB 中零污染。旧版本在量化检测通过后立即写入,会留下只有名字没有数据的孤立行,污染 Tab4/5/6。

### Pseudo-bulk 两步聚合(GQA 伪重复问题)
GQA 模型(如 LLaMA-3-8B 32Q/8KV)中,同一 KV head 下的多个 Q head 共享同一 K,
彼此强相关。若直接对所有头做 median,等价于对 KV head 的指标重复计数 group 次(伪重复)。
标准做法(Nature Comms 2021):
```
Step 1: groupby(layer, kv_head).median()  → 每KV head一个值,消除组内相关
Step 2: 对Step1结果做 median/mean         → 每层一个无偏代表值
```
实现在 `db/writer._pseudobulk_col()` 和 `core/plotter._aggregate_by_layer()`。
Tab3 Refresh 按钮触发 `refresh_all_summaries()` 自动将历史数据重算为 pseudo-bulk。

### 级联删除与写入权限统一验证
`delete_model()` 和所有写库操作均通过同一个 `check_write_permission(admin_token)` 验证,
后者对比环境变量 `WRITE_TOKEN`(HF Space Secrets 注入)。
删除顺序严格遵循外键依赖:`layer_head_metrics → model_summary → components → models`。
删除后返回各表实际删除行数,便于审计确认。

---

## 10. 部署说明

### HuggingFace Space 部署

1. 创建 Space,选择 Gradio SDK
2. 在 Space Settings 中添加 **Persistent Storage**(挂载到 `/data`)
   - `wang_laws.db` 重启后不丢失
3. 上传所有文件(保持目录结构)
4. 如需访问私有模型,在 Space Secrets 中设置 `HF_TOKEN`

#### 配置管理员写入权限(重要)

在 **Space Settings → Secrets** 中添加:

| Secret 名称   | 值               | 说明                            |
| ------------- | ---------------- | ------------------------------- |
| `WRITE_TOKEN` | 你自己设置的密码 | 管理员写库密钥,不进入 git repo |

**工作原理:**
```
HF Space Secrets(加密存储,不在 git 中)
    ↓ HF 运行时自动注入
Docker 容器环境变量 WRITE_TOKEN
    ↓ 服务端读取
os.environ.get("WRITE_TOKEN")
    ↓ 与用户输入的 Admin Token 比对(纯服务端,前端不可见)
True  → 写入数据库
False → 只读模式,分析正常运行
```

**三类用户的体验:**

| 用户            | Admin Write Token | 行为                               |
| --------------- | ----------------- | ---------------------------------- |
| 你(管理员)    | 填写正确密钥      | 分析结果写入数据库,排行榜更新     |
| 审稿人 / 复现者 | 留空              | 分析正常运行,指标完整显示,不写库 |
| 恶意用户        | 随意填写          | 分析可以跑,写库被拒绝             |

**未配置 `WRITE_TOKEN` 时:**
```python
# check_write_permission() 的行为:
server_token = os.environ.get("WRITE_TOKEN", "")
if not server_token:
    return False   # 服务端未配置 → 拒绝所有写入
```
即使有人猜到任意字符串也无法写入。

### 本地运行

```bash
pip install -r requirements.txt

# 可选:设置写入权限
export WRITE_TOKEN="your_secret_password"

python app.py
# 浏览器打开 http://127.0.0.1:7860
```

本地运行时数据库存于当前目录的 `wang_laws.db`。
不设置 `WRITE_TOKEN` 则所有人都是只读模式。
```

---

## 改动汇总

| 文件                    | 改动                                                                         |
| ----------------------- | ---------------------------------------------------------------------------- |
| `db/writer.py`          | 末尾追加 `check_write_permission()`,其余不变                                |
| `ui/tab_analyze.py`     | 完整重写:加 `admin_token` 参数,所有写库操作加 `can_write` 判断,日志改英文 |
| `README.md`             | 第10节部署说明扩充写权限配置说明                                             |
| `db/schema.py`          | 不变                                                                         |
| `db/reader.py`          | 不变                                                                         |
| `ui/tab_inspect.py`     | 不变                                                                         |
| `ui/tab_leaderboard.py` | 不变                                                                         |
| `ui/tab_database.py`    | 不变                                                                         |
| `app.py`                | 不变                                                                         |


### 注意事项

- 分析大模型(如 70B)时每层需要约 30 秒(受 HF CDN 网速限制)
- HF Space 免费版有 48 小时超时限制,建议开启断点续传分批分析
- 量化模型(GPTQ/AWQ/GGUF)自动拒绝,需使用原始 BF16 版本

---

## 11. 依赖清单

```
gradio>=4.0.0     # Web UI 框架
requests          # HTTP Range Request 读取远程权重
numpy             # 数值计算(统计汇总)
scipy             # spearman相关系数
torch             # SVD分解(torch.linalg.svd)
huggingface_hub   # list_repo_files(文件列表)
matplotlib        # 静态图导出(PNG/PDF/SVG,300dpi,论文级)
plotly            # 交互图(12×1全宽,浏览器内hover/zoom)
```

Python 内置(无需安装):
```
sqlite3    # 数据库
struct     # 解析safetensors header的8字节整数
json       # 解析safetensors header JSON
re         # 正则提取层号
datetime   # 时间戳
dataclasses # LayerProfile数据结构
```

---

## 12. 改动历史

### v0.1 — 初始版本(Tab1~4)
- Tab1 Inspect:模型结构探测,零下载,仅读 safetensors header
- Tab2 Analyze:HTTP Range Request 逐层分析,写库,断点续传
- Tab3 Leaderboard:Wang Score 排行榜
- Tab4 Database:数据库浏览,逐头原始数据查询

### v0.2 — 作图与论文表格(Tab5~6)
- 新增 `core/plotter.py`:matplotlib 4×3 静态图,18×20in @ 300dpi
- 新增 `core/plotter_plotly.py`:原生 Plotly 12×1 交互图,全宽自适应
- 新增 `ui/tab_plot.py`(Tab5):两条渲染路径(⚡Interactive / 🖨️Export)
- 新增 `core/table_gen.py`:6张论文表格生成(LaTeX/Markdown/CSV)
- 新增 `ui/tab_tables.py`(Tab6):一键生成,批量下载
- `app.py` 双语改造:英文在左,中文在右,两列并排表格

### v0.3 — Pseudo-bulk 聚合 + 防脏数据 + 级联删除
**问题修复:**
- `ui/tab_analyze.py``upsert_model` / `upsert_component` 推迟到 `load_all_shard_headers()` 成功后执行,防止模型名拼错产生脏数据

**新功能:**
- `db/writer.py`:新增 `_pseudobulk_col()``update_model_summary()` 改用 pseudo-bulk 两步聚合,消除 GQA 伪重复计数偏差
- `db/writer.py`:新增 `refresh_all_summaries()`,Tab3 Refresh 按钮触发,自动将历史数据重算为 pseudo-bulk
- `db/writer.py`:新增 `delete_model(conn, model_id, admin_token)`,级联删除模型所有数据,需 WRITE_TOKEN 验证
- `ui/tab_database.py`:新增 🗑️ Delete Model 区块,删除成功后自动刷新模型列表
- `ui/tab_inspect.py`:全文翻译为英文,逻辑不变

**改动文件汇总:**

| 文件                 | 类型 | 说明                                                    |
| -------------------- | ---- | ------------------------------------------------------- |
| `db/writer.py`       | 更新 | 新增 pseudo-bulk / refresh_all_summaries / delete_model |
| `ui/tab_analyze.py`  | 修复 | upsert_model 推迟到 shard headers 加载成功后            |
| `ui/tab_database.py` | 更新 | 新增删除模型 UI                                         |
| `ui/tab_inspect.py`  | 重构 | 全文英文化                                              |