Spaces:
Running
Running
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),实现跨模型推理能力排行。
[](https://doi.org/10.5281/zenodo.19707844)
[](https://hal.science/hal-05609398)
[](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` | 重构 | 全文英文化 | |