Alex W. commited on
Commit
6f797b1
·
1 Parent(s): 88f2eb5

add README for project.

Browse files
Files changed (1) hide show
  1. README.md +1002 -0
README.md CHANGED
@@ -13,3 +13,1005 @@ short_description: 'Compute SVD of LLM Q/K/V weights directly from Hugging Face
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ---
14
 
15
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
16
+
17
+ ---
18
+ # Wang's Five Laws — LLM Spectral Analyzer
19
+ ## 完整项目文档 README.md
20
+
21
+ ---
22
+
23
+
24
+ # 🔬 Wang's Five Laws — LLM Spectral Analyzer
25
+
26
+ **静态分析 LLM 注意力权重,无需推理,无需 benchmark,直接评估推理能力。**
27
+
28
+ 通过对 Q/K/V 权重矩阵做 SVD 分解,验证王氏五定律,
29
+ 计算 Wang Score(= 1 − median SSR_QK),实现跨模型推理能力排行。
30
+
31
+ [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.19707844-blue)](https://doi.org/10.5281/zenodo.19707844)
32
+ [![HAL](https://img.shields.io/badge/HAL-hal--05609398-red)](https://hal.science/hal-05609398)
33
+ [![Wang's Law](https://img.shields.io/badge/Wang%27s%20Law-r%3D1-blue)](https://github.com/emis-framework/math-under-llm)
34
+
35
+ ---
36
+
37
+ ## 目录
38
+
39
+ 1. [项目背景](#1-项目背景)
40
+ 2. [王氏五定律速查](#2-王氏五定律速查)
41
+ 3. [整体架构](#3-整体架构)
42
+ 4. [目录结构](#4-目录结构)
43
+ 5. [各层详细说明](#5-各层详细说明)
44
+ - 5.1 [core 层](#51-core-层——计算引擎)
45
+ - 5.2 [db 层](#52-db-层——数据持久化)
46
+ - 5.3 [ui 层](#53-ui-层——用户界面)
47
+ - 5.4 [app.py 入口](#54-apppy——主入口)
48
+ 6. [数据库表结构](#6-数据库表结构)
49
+ 7. [数据流全链路](#7-数据流全链路)
50
+ 8. [函数调用关系图](#8-函数调用关系图)
51
+ 9. [关键设计决策](#9-关键设计决策)
52
+ 10. [部署说明](#10-部署说明)
53
+ 11. [依赖清单](#11-依赖清单)
54
+
55
+ ---
56
+
57
+ ## 1. 项目背景
58
+
59
+ 传统评估 LLM 推理能力需要跑 benchmark(耗时、昂贵、可刷榜)。
60
+
61
+ 本项目发现:**只看权重矩阵的奇异值分解(SVD)结构,
62
+ 就能静态评估模型推理质量**,无需任何推理。
63
+
64
+ 核心原理:
65
+ - 对每一层注意力的 Q、K、V 权重矩阵做 SVD
66
+ - 计算奇异值谱之间的相关性、形状残差、子空间对齐度
67
+ - 这些指标与模型推理能力高度相关(经多个模型验证)
68
+
69
+ **运行方式**:HTTP Range Request 直接读取 HuggingFace 远程权重,
70
+ 无需下载整个模型(一个 14B 模型只需读取约 200MB 数据而非 28GB)。
71
+
72
+ ---
73
+
74
+ ## 2. 王氏五定律速查
75
+
76
+ | 定律 | 名称 | 公式 | 理论极值 | 实测范围 |
77
+ | -------- | ------------------ | ----------------------------------- | ---------- | ------------ |
78
+ | 第一定律 | 谱线性对齐 | Pearson r(s_Q, s_K) | → 1 | 0.94~0.99 |
79
+ | 第二定律 | 谱形状残差 | SSR = mean\|ŝ_Q − ŝ_K\| | → 0 | 0.006~0.016 |
80
+ | 第三定律 | 精度-深度约束 | L_max = min(L_info, L_quant, L_dyn) | 由精度决定 | FP16→16层 |
81
+ | 第四定律 | 输出子空间解耦 | cosU(U_Q,U_V) < 1/√d_head | 超正交 | ~20%低于随机 |
82
+ | 第五定律 | 输入子空间随机正交 | cosV ≈ 1/√d_model | ≈随机基线 | 符合理论 |
83
+
84
+ **Wang Score = 1 − median(SSR_QK)**(越高越好,理论极值=1)
85
+
86
+ ---
87
+
88
+ ## 3. 整体架构
89
+
90
+ ```
91
+ ┌─────────────────────────────────────────────────────┐
92
+ │ app.py │
93
+ │ 主入口,组装所有 Tab │
94
+ │ 启动时调用 init_db() │
95
+ └──────┬──────────────────────────────────────────────┘
96
+ │ 调用
97
+
98
+ ┌──────────────────────────────────────────────────────┐
99
+ │ ui/ 层 │
100
+ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
101
+ │ │tab_inspect │ │tab_analyze │ │tab_leaderbd │ │
102
+ │ │结构探测 │ │分析+写库 │ │排行榜 │ │
103
+ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
104
+ │ │ │ │ │
105
+ │ └────────────────┼────────────────┘ │
106
+ │ ┌─────────────┐ │ │
107
+ │ │tab_database │─────────┘ │
108
+ │ │数据库浏览 │ │
109
+ │ └─────────────┘ │
110
+ └─────��┬──────────────────────────┬───────────────────┘
111
+ │ 调用 │ 调用
112
+ ▼ ▼
113
+ ┌─────────────────┐ ┌─────────────────────────────┐
114
+ │ core/ 层 │ │ db/ 层 │
115
+ │ │ │ │
116
+ │ fetcher.py │ │ schema.py writer.py │
117
+ │ 远程读取权重 │ │ 建表 写入数据 │
118
+ │ │ │ │
119
+ │ layer_profile.py│ │ reader.py │
120
+ │ 推断层结构 │ │ 查询数据 │
121
+ │ │ │ │
122
+ │ metrics.py │ │ SQLite 文件 │
123
+ │ 计算五定律 │ │ /data/wang_laws.db │
124
+ └─────────────────┘ └─────────────────────────────┘
125
+ ```
126
+
127
+ **三层职责:**
128
+
129
+ | 层 | 职责 | 不做什么 |
130
+ | ------- | --------------------------- | ---------------------- |
131
+ | `core/` | 纯计算,无 UI,无 DB | 不写数据库,不渲染界面 |
132
+ | `db/` | 纯数据库操作 | 不做计算,不渲染界面 |
133
+ | `ui/` | 纯界面逻辑,调用 core 和 db | 不做底层计算 |
134
+
135
+ ---
136
+
137
+ ## 4. 目录结构
138
+
139
+ ```
140
+ 项目根目录/
141
+
142
+ ├── app.py # 主入口:初始化DB,组装4个Tab
143
+ ├── requirements.txt # 依赖清单
144
+
145
+ ├── core/ # 计算引擎(纯Python,无副作用)
146
+ │ ├── __init__.py # 空文件
147
+ │ ├── config.py # 全局开关(DEBUG=True/False)
148
+ │ ├── debug.py # 调试输出工具(受config.DEBUG控制)
149
+ │ ├── fetcher.py # HTTP Range Request 读取远程权重
150
+ │ ├── layer_profile.py # 自动推断模型层结构
151
+ │ └── metrics.py # 计算王氏五定律全部指标
152
+
153
+ ├── db/ # 数据持久化层
154
+ │ ├── __init__.py # 空文件
155
+ │ ├── schema.py # 建表SQL + 数据库连接
156
+ │ ├── writer.py # 写入分析结果 + 断点续传检查
157
+ │ └── reader.py # 查询排行榜、模型详情、原始数据
158
+
159
+ └── ui/ # Gradio 界面层
160
+ ├── __init__.py # 空文件
161
+ ├── tab_inspect.py # Tab1:模型结构探测
162
+ ├── tab_analyze.py # Tab2:分析模型 + 写库
163
+ ├── tab_leaderboard.py # Tab3:王氏评分排行榜
164
+ └── tab_database.py # Tab4:数据库浏览
165
+ ```
166
+
167
+ ---
168
+
169
+ ## 5. 各层详细说明
170
+
171
+ ### 5.1 `core/` 层——计算引擎
172
+
173
+ #### `core/config.py`
174
+
175
+ ```python
176
+ DEBUG = False # True → 打印详细调试信息;False → 静默运行
177
+ ```
178
+
179
+ 全局唯一开关。所有调试输出都受这个控制。
180
+
181
+ ---
182
+
183
+ #### `core/debug.py`
184
+
185
+ | 函数 | 签名 | 用途 |
186
+ | -------- | ------------------------------ | -------------------------------------- |
187
+ | `dlog` | `(lines: list[str], msg: str)` | 向日志列表追加调试信息(仅DEBUG=True) |
188
+ | `dprint` | `(msg: str)` | 打印到stdout(仅DEBUG=True) |
189
+
190
+ `dlog` 用于 `metrics.py` 和 `tab_analyze.py`(有 lines 列表的地方)。
191
+ `dprint` 用于 `fetcher.py`(没有 lines 列表的地方)。
192
+
193
+ ---
194
+
195
+ #### `core/fetcher.py`
196
+
197
+ **核心思想**:safetensors 文件头记录了每个 tensor 的字节偏移,
198
+ 用 HTTP Range Request 只下载需要的字节,无需下载整个文件。
199
+
200
+ | 函数 | 签名 | 返回 | 用途 |
201
+ | ------------------------- | ------------------------------------------------ | -------------------------------- | -------------------------- |
202
+ | `get_file_url` | `(model_id, filename)` | `str` | 拼接 HF 下载 URL |
203
+ | `read_safetensors_header` | `(url, token)` | `(header_dict, header_size)` | 读取文件头(两次HTTP请求) |
204
+ | `load_tensor_remote` | `(url, tensor_name, header, header_size, token)` | `torch.Tensor` | 按名读取单个tensor |
205
+ | `get_safetensor_files` | `(model_id, token)` | `list[str]` | 列出所有.safetensors文件 |
206
+ | `find_index_file` | `(model_id, token)` | `dict\|None` | 读取分片索引文件 |
207
+ | `get_all_shard_files` | `(model_id, token)` | `list[str]` | 获取全部分片文件名 |
208
+ | `load_all_shard_headers` | `(model_id, token)` | `dict[filename, (header, size)]` | 读取所有分片的header |
209
+ | `check_quantization` | `(model_id, token)` | `(is_blocked, message)` | 三重量化检测 |
210
+ | `http_error_msg` | `(e, model_id)` | `str` | HTTP错误码转中文提示 |
211
+
212
+ **`load_all_shard_headers` 返回结构:**
213
+ ```python
214
+ {
215
+ "model-00001-of-00006.safetensors": (header_dict, header_size),
216
+ "model-00002-of-00006.safetensors": (header_dict, header_size),
217
+ ...
218
+ }
219
+ # header_dict 结构:
220
+ {
221
+ "model.layers.0.self_attn.q_proj.weight": {
222
+ "dtype": "BF16",
223
+ "shape": [4096, 4096],
224
+ "data_offsets": [0, 33554432]
225
+ },
226
+ ...
227
+ }
228
+ ```
229
+
230
+ **量化检测四重逻辑(按顺序):**
231
+ 1. 检测 `config.json` 中的 `quantization_config` 字段
232
+ 2. 检测模型名是否含 `gptq/awq/gguf` 关键词
233
+ 3. 检测文件列表是否有 `.gguf` 文件
234
+ 4. 检测 header 中是否有量化专用 key(如 `qweight`, `qzeros`)
235
+
236
+ ---
237
+
238
+ #### `core/layer_profile.py`
239
+
240
+ **核心思想**:从权重文件的 key 名自动推断模型结构,零 hard coding,
241
+ 不依赖模型名称或配置文件(配置文件只是辅助参考)。
242
+
243
+ **关键数据结构:**
244
+
245
+ ```python
246
+ @dataclass
247
+ class QKVKey:
248
+ shard: str # 所在分片文件名,如 "model-00001-of-00006.safetensors"
249
+ key: str # 完整tensor名,如 "model.layers.0.self_attn.q_proj.weight"
250
+ shape: list # tensor形状,如 [4096, 4096]
251
+
252
+ @dataclass
253
+ class LayerProfile:
254
+ prefix: str # 组件前缀,如 "model.language_model."
255
+ layer_idx: int # 层号(原始safetensors key中的N)
256
+ q: QKVKey # Q权重位置
257
+ k: QKVKey # K权重位置
258
+ v: QKVKey|None # V权重位置(None表示K=V共享)
259
+ head_dim: int # 每个head的维度
260
+ n_q_heads: int # Q head数量
261
+ n_kv_heads: int # KV head数量(GQA时 < n_q_heads)
262
+ d_model: int # 模型隐层维度(= q_shape[1])
263
+ kv_shared: bool # True = K和V共享(如Gemma全局层)
264
+ complete: bool # True = Q/K都存在且head_dim推断成功
265
+ infer_ok: bool # head_dim推断是否成功
266
+ head_dim_source: str # 推断来源:"k_norm"/"q_norm"/"config"/"enum"
267
+ ```
268
+
269
+ | 函数 | 签名 | 返回 | 用途 |
270
+ | ----------------------- | ------------------------------------ | ------------------------------------ | --------------------------------------------------- |
271
+ | `classify_qkv_suffix` | `(suffix: str)` | `'q'/'k'/'v'/None` | 从key后缀判断是Q/K/V |
272
+ | `is_norm_key` | `(suffix: str)` | `bool` | 判断是否为norm key(辅助推断head_dim) |
273
+ | `scan_model_structure` | `(all_shard_headers, config_params)` | `dict[(prefix,layer), LayerProfile]` | **核心函数**:扫描全部headers,构建LayerProfile字典 |
274
+ | `summarize_structure` | `(profiles)` | `str` | 生成人类可读的结构报告(Tab1使用) |
275
+ | `extract_config_params` | `(config: dict)` | `dict` | 从config.json提取关键参数(兼容Gemma4嵌套结构) |
276
+
277
+ **`scan_model_structure` 工作流程:**
278
+ ```
279
+ 第一遍扫描:遍历所有shard的所有key
280
+ → 用正则 r'layers\.(\d+)\.' 提取层号
281
+ → prefix = key的layers.N.之前部分
282
+ → suffix = key的layers.N.之后部分
283
+ → classify_qkv_suffix(suffix) → 归类为Q/K/V
284
+ → is_norm_key(suffix) → 收集k_norm/q_norm形状(辅助推断head_dim)
285
+
286
+ 第二遍构建:对每个(prefix, layer_idx)槽
287
+ → 检查Q/K是否都存在(必要条件)
288
+ → V不存在 → kv_shared=True
289
+ → _infer_head_dim() → 推断head_dim(5个优先级)
290
+ → 计算n_q_heads, n_kv_heads, d_model
291
+ → 构建LayerProfile
292
+ ```
293
+
294
+ **head_dim 推断优先级:**
295
+ ```
296
+ 1. k_norm.shape[0] ← 最可靠(Gemma系列有这个)
297
+ 2. q_norm.shape[0] ← 备用
298
+ 3. config["head_dim"] ← config.json直接给出
299
+ 4. config["hidden_size"] / config["num_attention_heads"] ← 计算得出
300
+ 5. 枚举候选值 [512,256,128,96,80,64,48,40,32,16] ← 最后手段
301
+ ```
302
+
303
+ **KV共享检测(Gemma全局层):**
304
+ ```
305
+ V的key不存在于任何shard header → kv_shared=True → layer_type="global"
306
+ ```
307
+
308
+ ---
309
+
310
+ #### `core/metrics.py`
311
+
312
+ 对一层的Q/K/V权重矩阵计算所有指标。
313
+
314
+ **底层计算函数:**
315
+
316
+ | 函数 | 签名 | 返回 | 对应定律 |
317
+ | ------------- | ---------------------- | ------------------- | ---------------------- |
318
+ | `pearson` | `(a, b: Tensor)` | `float` | 第一定律 |
319
+ | `spearman_r` | `(a, b: Tensor)` | `float` | 第一定律(补充) |
320
+ | `ssr` | `(a, b: Tensor)` | `float` | 第二定律 |
321
+ | `svr` | `(a, b: Tensor)` | `(alpha, residual)` | 尺度因子 |
322
+ | `cos_U` | `(U_a, U_b: Tensor)` | `float` | 第四定律(左奇异向量) |
323
+ | `cos_V` | `(Vt_a, Vt_b: Tensor)` | `float` | 第五定律(右奇异向量) |
324
+ | `sigma_stats` | `(s: Tensor)` | `(max, min, cond)` | 第三定律 |
325
+
326
+ **`ssr` 计算细节:**
327
+ ```python
328
+ # 归一化后逐元素绝对差的均值
329
+ n = min(len(a), len(b))
330
+ an = a[:n] / ||a[:n]|| # L2归一化
331
+ bn = b[:n] / ||b[:n]||
332
+ SSR = mean(|an - bn|)
333
+ ```
334
+
335
+ **主分析函数:**
336
+
337
+ ```python
338
+ def analyze_layer(
339
+ W_q: torch.Tensor, # shape: [n_q_heads * head_dim, d_model]
340
+ W_k: torch.Tensor, # shape: [n_kv_heads * head_dim, d_model]
341
+ W_v: torch.Tensor, # shape: [n_kv_heads * head_dim, d_model]
342
+ profile: LayerProfile,
343
+ ) -> tuple[list[dict], str]:
344
+ # 返回:(records列表, 格式化日志字符串)
345
+ ```
346
+
347
+ **`analyze_layer` 工作流程:**
348
+ ```
349
+ 对每个 kv_head(0 ~ n_kv_heads-1):
350
+ 切片:k_t = W_k[kv_h*d_head : (kv_h+1)*d_head, :]
351
+ SVD:U_k, s_k, Vt_k = svd(k_t)
352
+ 计算 sigma_stats(s_k)
353
+
354
+ 对每个 q_head(属于这个kv_head的group,GQA时 group = n_q/n_kv):
355
+ 切片:q_t = W_q[h*d_head : (h+1)*d_head, :]
356
+ SVD:U_q, s_q, Vt_q = svd(q_t)
357
+
358
+ 计算所有指标:
359
+ pearson_QK, spearman_QK, ssr_QK, alpha_QK ← Q vs K奇异值
360
+ pearson_QV, ssr_QV, alpha_QV ← Q vs V奇异值
361
+ ssr_KV, alpha_KV ← K vs V奇异值
362
+ cosU_QK, cosU_QV, cosU_KV ← 左奇异向量
363
+ cosV_QK, cosV_QV, cosV_KV ← 右奇异向量
364
+ sigma_max/min/cond for Q, K, V
365
+
366
+ append到records
367
+
368
+ 特殊处理:kv_shared=True时,KV指标设为理论值(ssr=0, pearson=1, cosU=1等)
369
+ ```
370
+
371
+ **`records` 每条记录的字段(共37个字段):**
372
+ ```python
373
+ {
374
+ "prefix": str, "layer": int,
375
+ "kv_head": int, "q_head": int,
376
+ "kv_shared": bool, "head_dim": int,
377
+ "d_model": int, "n_q_heads": int, "n_kv_heads": int,
378
+ # 第一定律
379
+ "pearson_QK": float, "spearman_QK": float,
380
+ "pearson_QV": float, "pearson_KV": float,
381
+ # 第二定律
382
+ "ssr_QK": float, "ssr_QV": float, "ssr_KV": float,
383
+ # 第三定律
384
+ "sigma_max_Q": float, "sigma_min_Q": float, "cond_Q": float,
385
+ "sigma_max_K": float, "sigma_min_K": float, "cond_K": float,
386
+ "sigma_max_V": float, "sigma_min_V": float, "cond_V": float,
387
+ # 第四定律
388
+ "cosU_QK": float, "cosU_QV": float, "cosU_KV": float,
389
+ # 第五定律
390
+ "cosV_QK": float, "cosV_QV": float, "cosV_KV": float,
391
+ # 尺度因子
392
+ "alpha_QK": float, "alpha_QV": float, "alpha_KV": float,
393
+ "alpha_res_QK": float, "alpha_res_QV": float, "alpha_res_KV": float,
394
+ }
395
+ ```
396
+
397
+ ```python
398
+ def summarize_records(records: list[dict], model_id: str) -> str:
399
+ # 对records做统计汇总,返回格式化文本
400
+ # 按prefix分组,对每个指标计算 Median/Mean/Min/Max
401
+ # KV指标自动排除kv_shared=True的行(避免理论值污染统计)
402
+ ```
403
+
404
+ ---
405
+
406
+ ### 5.2 `db/` 层——数据持久化
407
+
408
+ #### `db/schema.py`
409
+
410
+ 数据库路径逻辑:
411
+ ```python
412
+ def get_db_path() -> str:
413
+ if os.path.exists("/data"): # HF Space bucket挂载点
414
+ return "/data/wang_laws.db"
415
+ return "wang_laws.db" # 本地开发回退
416
+ ```
417
+
418
+ | 函数 | 签名 | 用途 |
419
+ | ---------------- | -------- | ---------------------------------- |
420
+ | `get_db_path` | `()` | 返回数据库文件路径 |
421
+ | `get_connection` | `()` | 返回SQLite连接(WAL模式,Row工厂) |
422
+ | `init_db` | `()` | 建表+建索引,幂等,返回连接 |
423
+ | `get_db_stats` | `(conn)` | 返回各表行数+文件大小 |
424
+
425
+ ---
426
+
427
+ #### `db/writer.py`
428
+
429
+ | 函数 | 签名 | 用途 |
430
+ | ---------------------- | --------------------------------------------------- | ------------------------------------------------ |
431
+ | `infer_layer_type` | `(kv_shared: bool)` | `True→"global"`, `False→"standard"` |
432
+ | `get_analyzed_layers` | `(conn, model_id, prefix)` | 返回已完成的层号集合(断点续传用) |
433
+ | `is_layer_complete` | `(conn, model_id, prefix, layer, expected_records)` | 检查某层记录数是否达到预期 |
434
+ | `upsert_model` | `(conn, model_id, model_type, notes)` | 写入/更新模型元数据 |
435
+ | `upsert_component` | `(conn, model_id, prefix, n_layers, ...)` | 写入/更新组件信息 |
436
+ | `write_layer_records` | `(conn, model_id, records: list[dict])` | 批量写入一层的逐头数据(INSERT OR REPLACE) |
437
+ | `update_model_summary` | `(conn, model_id, prefix)` | 重算并写入model_summary的all/standard/global三行 |
438
+
439
+ **`update_model_summary` 逻辑:**
440
+ ```
441
+ 对 layer_type in ["all", "standard", "global"]:
442
+ 从 layer_head_metrics 查对应行
443
+ 计算各指标的 median/mean
444
+ wang_score 统一用 standard 层的 median(ssr_QK) 计算
445
+ (即使写 all/global 行,wang_score 也来自 standard 层)
446
+ INSERT OR REPLACE 写入 model_summary
447
+ ```
448
+
449
+ ---
450
+
451
+ #### `db/reader.py`
452
+
453
+ | 函数 | 签名 | 返回 | 用途 |
454
+ | --------------------- | -------------------------------------------------------------- | -------------- | ---------------------------- |
455
+ | `get_leaderboard` | `(conn, prefix_filter, layer_type, limit)` | `pd.DataFrame` | 排行榜查询,按wang_score降序 |
456
+ | `get_model_summary` | `(conn, model_id)` | `pd.DataFrame` | 某模型所有组件的汇总统计 |
457
+ | `get_layer_metrics` | `(conn, model_id, prefix, layer_type, start_layer, end_layer)` | `pd.DataFrame` | 逐头原始数据查询 |
458
+ | `get_analyzed_models` | `(conn)` | `pd.DataFrame` | 所有已分析模型列表 |
459
+ | `get_resume_status` | `(conn, model_id, prefix)` | `dict` | 断点续传状态:已完成层号集合 |
460
+
461
+ ---
462
+
463
+ ### 5.3 `ui/` 层——用户界面
464
+
465
+ #### `ui/tab_inspect.py` — Tab1:结构探测
466
+
467
+ **函数:**
468
+
469
+ ```python
470
+ def inspect_model(model_id, hf_token, progress) -> (str, pd.DataFrame):
471
+ """
472
+ 工作流程:
473
+ 1. check_quantization() ← 量化检测,失败则返回
474
+ 2. 读取 config.json ← extract_config_params()
475
+ 3. load_all_shard_headers() ← 读取所有分片header
476
+ 4. scan_model_structure() ← 构建LayerProfile字典
477
+ 5. summarize_structure() ← 生成文本报告
478
+ 6. 构建概览DataFrame ← 每层一行
479
+ 返回:(日志文本, 层结构DataFrame)
480
+ """
481
+
482
+ def build_tab_inspect() -> (inspect_model_id, inspect_token):
483
+ """
484
+ 构建Tab1的Gradio组件
485
+ 返回:(model_id文本框, token文本框)
486
+ ← 返回值供app.py做Tab1→Tab2的联动同步
487
+ """
488
+ ```
489
+
490
+ **UI组件:**
491
+ ```
492
+ 模型ID输入框 + Token输入框 + 探测按钮
493
+ → 日志文本框(结构报告)
494
+ → 层结构表格(prefix/layer/d_model/head_dim/n_q/n_kv/kv_shared等)
495
+ ```
496
+
497
+ ---
498
+
499
+ #### `ui/tab_analyze.py` — Tab2:分析(核心Tab)
500
+
501
+ **函数:**
502
+
503
+ ```python
504
+ def run_analysis(model_id, hf_token, start_layer, end_layer, progress)
505
+ -> (str, pd.DataFrame):
506
+ """
507
+ 完整工作流程:
508
+
509
+ [准备阶段]
510
+ 1. init_db() ← 获取DB连接
511
+ 2. check_quantization() ← 量化检测
512
+ 3. 读取 config.json
513
+ 4. upsert_model() ← 写模型元数据到DB
514
+ 5. load_all_shard_headers() ← 读所有分片header
515
+ 6. scan_model_structure() ← 构建LayerProfile字典
516
+ 7. upsert_component() for each prefix ← 写组件信息到DB
517
+ 8. 按 start_layer~end_layer 过滤层
518
+
519
+ [断点续传检查]
520
+ 9. get_analyzed_layers() for each prefix
521
+ → done_layers: dict[prefix, set[int]]
522
+ → 打印待分析层和已跳过层
523
+
524
+ [逐层分析循环]
525
+ for each (prefix, layer_idx) in filtered(按prefix+layer排序):
526
+ 10. 检查:layer_idx in done_layers[prefix] → continue(跳过)
527
+ 11. load_tensor_remote(Q) ← HTTP Range Request
528
+ 12. load_tensor_remote(K)
529
+ 13. kv_shared ? W_v=W_k.clone() : load_tensor_remote(V)
530
+ 14. analyze_layer(W_q, W_k, W_v, prof) ← 计算五定律
531
+ 15. write_layer_records(conn, model_id, records) ← 写DB
532
+ 16. update_model_summary(conn, model_id, prefix) ← 更新排行榜
533
+ 17. del W_q, W_k, W_v ← 释放内存
534
+
535
+ [收尾]
536
+ 18. 更新 models.analyze_sec(总耗时)
537
+ 19. summarize_records() ← 生成汇总文本
538
+ 返回:(日志文本, 逐头结果DataFrame)
539
+ """
540
+
541
+ def build_tab_analyze() -> (model_id_input, token_input):
542
+ """构建Tab2的Gradio组件,返回值供app.py联动"""
543
+ ```
544
+
545
+ **UI组件:**
546
+ ```
547
+ 模型ID + Token + 起始层号 + 结束层号 + 分析按钮
548
+ 侧边栏:推荐模型列表 + 层号说明
549
+ → 分析日志文本框(逐头详情)
550
+ → 逐头结果表格(37列全指标)
551
+ ```
552
+
553
+ ---
554
+
555
+ #### `ui/tab_leaderboard.py` — Tab3:排行榜
556
+
557
+ **函数:**
558
+
559
+ ```python
560
+ def _format_leaderboard(df: pd.DataFrame) -> pd.DataFrame:
561
+ """
562
+ 格式化显示:
563
+ - model_id → model_name(取最后一段)
564
+ - wang_score → wang_score_pct(百分制字符串)
565
+ - 数值列 → 6位小数字符串
566
+ - 选择展示列(隐藏冗余列)
567
+ """
568
+
569
+ def load_leaderboard(prefix_filter, layer_type) -> (pd.DataFrame, str):
570
+ """
571
+ 调用 reader.get_leaderboard()
572
+ prefix_filter 空字符串 → None(不过滤)
573
+ layer_type="all" → 实际查 "standard"(排行榜默认用standard)
574
+ """
575
+
576
+ def build_tab_leaderboard():
577
+ """
578
+ UI:组件过滤输入框 + 层类型下拉 + 刷新按钮
579
+ → 状态文本 + 排行榜表格 + 指标说明
580
+ 用户手动点刷新(不自动加载)
581
+ """
582
+ ```
583
+
584
+ ---
585
+
586
+ #### `ui/tab_database.py` — Tab4:数据库浏览
587
+
588
+ **函数:**
589
+
590
+ ```python
591
+ def load_db_stats() -> str:
592
+ """调用 get_db_stats(),返回各表行数+文件大小"""
593
+
594
+ def load_model_list() -> pd.DataFrame:
595
+ """调用 get_analyzed_models(),返回模型列表"""
596
+
597
+ def load_model_detail(model_id) -> (pd.DataFrame, str):
598
+ """
599
+ 调用 get_model_summary() → summary_df
600
+ 调用 get_resume_status() for each prefix → 断点续传状态文本
601
+ """
602
+
603
+ def load_layer_data(model_id, prefix, layer_type, start_layer, end_layer)
604
+ -> (pd.DataFrame, str):
605
+ """调用 get_layer_metrics(),返回逐头原始数据"""
606
+
607
+ def build_tab_database():
608
+ """
609
+ UI分为4个区块:
610
+ 1. 数据库统计(行数+文件大小)
611
+ 2. 已分析模型列表
612
+ 3. 模型详情+断点续传状态
613
+ 4. 逐头原始数据查询(支持按prefix/layer_type/层号范围过滤)
614
+ """
615
+ ```
616
+
617
+ ---
618
+
619
+ ### 5.4 `app.py`——主入口
620
+
621
+ ```python
622
+ # 启动时执行(模块级)
623
+ init_db() # 建表,幂等
624
+
625
+ # Gradio Blocks
626
+ with gr.Blocks(...) as demo:
627
+ # 标题 + 五定律表格 + DOI徽章
628
+
629
+ with gr.Tabs():
630
+ inspect_model_id, inspect_token = build_tab_inspect()
631
+ analyze_model_id, analyze_token = build_tab_analyze()
632
+ build_tab_leaderboard()
633
+ build_tab_database()
634
+
635
+ # Tab1 → Tab2 联动(避免重复输入)
636
+ inspect_model_id.change(fn=lambda x:x,
637
+ inputs=inspect_model_id, outputs=analyze_model_id)
638
+ inspect_token.change(fn=lambda x:x,
639
+ inputs=inspect_token, outputs=analyze_token)
640
+ ```
641
+
642
+ ---
643
+
644
+ ## 6. 数据库表结构
645
+
646
+ 共4张表,SQLite 存储于 `/data/wang_laws.db`。
647
+
648
+ ### `models` — 模型基本信息
649
+ ```sql
650
+ CREATE TABLE models (
651
+ model_id TEXT PRIMARY KEY, -- "google/gemma-4-e2b"
652
+ model_type TEXT, -- "gemma4" / "qwen2" 等
653
+ analyzed_at TIMESTAMP, -- 最后分析时间
654
+ analyze_sec REAL, -- 本次分析总耗时(秒)
655
+ notes TEXT -- 备注
656
+ );
657
+ ```
658
+
659
+ ### `components` — 组件信息
660
+ ```sql
661
+ CREATE TABLE components (
662
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
663
+ model_id TEXT NOT NULL,
664
+ prefix TEXT NOT NULL, -- "model.language_model."
665
+ n_layers INTEGER, -- 该组件完整层数
666
+ head_dim_min INTEGER, -- 最小head_dim(异构层存在时有意义)
667
+ head_dim_max INTEGER, -- 最大head_dim
668
+ has_kv_shared INTEGER DEFAULT 0, -- 是否有K=V共享层
669
+ has_global INTEGER DEFAULT 0, -- 是否有global层
670
+ d_model INTEGER, -- 输入维度
671
+ UNIQUE(model_id, prefix)
672
+ );
673
+ ```
674
+
675
+ ### `layer_head_metrics` — 逐头原始数据(主数据表)
676
+ ```sql
677
+ CREATE TABLE layer_head_metrics (
678
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
679
+ model_id TEXT NOT NULL,
680
+ prefix TEXT NOT NULL,
681
+ layer INTEGER NOT NULL,
682
+ layer_type TEXT DEFAULT 'standard', -- "standard" / "global"
683
+ kv_head INTEGER NOT NULL,
684
+ q_head INTEGER NOT NULL,
685
+ kv_shared INTEGER DEFAULT 0, -- 1=K=V共享(理论值),0=正常
686
+ head_dim INTEGER,
687
+ d_model INTEGER,
688
+ n_q_heads INTEGER,
689
+ n_kv_heads INTEGER,
690
+ -- 第一定律
691
+ pearson_QK REAL, spearman_QK REAL, pearson_QV REAL, pearson_KV REAL,
692
+ -- 第二定律
693
+ ssr_QK REAL, ssr_QV REAL, ssr_KV REAL,
694
+ -- 第三定律
695
+ sigma_max_Q REAL, sigma_min_Q REAL, cond_Q REAL,
696
+ sigma_max_K REAL, sigma_min_K REAL, cond_K REAL,
697
+ sigma_max_V REAL, sigma_min_V REAL, cond_V REAL,
698
+ -- 第四定律
699
+ cosU_QK REAL, cosU_QV REAL, cosU_KV REAL,
700
+ -- 第五定律
701
+ cosV_QK REAL, cosV_QV REAL, cosV_KV REAL,
702
+ -- 尺度因子
703
+ alpha_QK REAL, alpha_res_QK REAL,
704
+ alpha_QV REAL, alpha_res_QV REAL,
705
+ alpha_KV REAL, alpha_res_KV REAL,
706
+
707
+ UNIQUE(model_id, prefix, layer, kv_head, q_head)
708
+ );
709
+ ```
710
+
711
+ ### `model_summary` — 汇总统计(排行榜用)
712
+ ```sql
713
+ CREATE TABLE model_summary (
714
+ model_id TEXT NOT NULL,
715
+ prefix TEXT NOT NULL,
716
+ layer_type TEXT NOT NULL DEFAULT 'all', -- all/standard/global
717
+ -- 第一定律
718
+ median_pearson_QK REAL, mean_pearson_QK REAL,
719
+ -- 第二定律
720
+ median_ssr_QK REAL, mean_ssr_QK REAL,
721
+ median_ssr_QV REAL, mean_ssr_QV REAL,
722
+ -- 第三定律
723
+ median_cond_Q REAL, mean_cond_Q REAL,
724
+ -- 第四定律
725
+ median_cosU_QK REAL, median_cosU_QV REAL,
726
+ -- 第五定律
727
+ median_cosV_QK REAL, median_cosV_QV REAL,
728
+ -- 王氏评分(始终用standard层计算,即使layer_type=all/global)
729
+ wang_score REAL,
730
+ n_layers INTEGER,
731
+ n_records INTEGER,
732
+ updated_at TIMESTAMP,
733
+
734
+ PRIMARY KEY(model_id, prefix, layer_type)
735
+ );
736
+ ```
737
+
738
+ **每个(model_id, prefix)在model_summary中有3行:**
739
+ ```
740
+ (model_id, prefix, "all") ← 全部层混合统计
741
+ (model_id, prefix, "standard") ← 只含standard层
742
+ (model_id, prefix, "global") ← 只含global层(如Gemma全局层)
743
+ ```
744
+
745
+ **layer_type 推断规则(零hard coding):**
746
+ ```
747
+ kv_shared=True → layer_type="global"
748
+ kv_shared=False → layer_type="standard"
749
+ ```
750
+
751
+ ---
752
+
753
+ ## 7. 数据流全链路
754
+
755
+ ```
756
+ 用户输入模型ID(如 "google/gemma-4-e2b")
757
+
758
+
759
+ [Tab1 或 Tab2]
760
+ check_quantization()
761
+ → 检测config.json / 模型名 / 文件列表 / header内容
762
+ → 量化模型直接拒绝
763
+
764
+
765
+ load_all_shard_headers()
766
+ → 对每个.safetensors文件:
767
+ HTTP GET bytes=0-7 → header_size(8字节小端整数)
768
+ HTTP GET bytes=8-{8+size} → JSON header
769
+ → 返回 {filename: (header_dict, header_size)}
770
+
771
+
772
+ scan_model_structure()
773
+ → 两遍扫描所有key → 构建 {(prefix,layer): LayerProfile}
774
+ → 自动推断:head_dim / n_q_heads / n_kv_heads / kv_shared
775
+
776
+ ▼(Tab2专有)
777
+ 断点续传检查
778
+ → get_analyzed_layers() → done_layers: dict[prefix, set[int]]
779
+
780
+ ▼(逐层循环)
781
+ load_tensor_remote(W_q) → HTTP GET bytes={abs_start}-{abs_end}
782
+ load_tensor_remote(W_k) → 同上
783
+ load_tensor_remote(W_v) → 同上(kv_shared时直接clone W_k)
784
+
785
+
786
+ analyze_layer(W_q, W_k, W_v, profile)
787
+ → 按head切片
788
+ → SVD分解每个head
789
+ → 计算37个指标
790
+ → 返回 records: list[dict]
791
+
792
+
793
+ write_layer_records(conn, model_id, records)
794
+ → INSERT OR REPLACE 批量写入 layer_head_metrics
795
+
796
+
797
+ update_model_summary(conn, model_id, prefix)
798
+ → 查询 layer_head_metrics
799
+ → 计算 median/mean
800
+ → wang_score = 1 - median(ssr_QK) [用standard层]
801
+ → INSERT OR REPLACE 写入 model_summary(all/standard/global 3行)
802
+
803
+
804
+ [Tab3 排行榜]
805
+ get_leaderboard()
806
+ → SELECT from model_summary WHERE layer_type='standard'
807
+ → ORDER BY wang_score DESC
808
+ → 格式化展示
809
+ ```
810
+
811
+ ---
812
+
813
+ ## 8. 函数调用关系图
814
+
815
+ ```
816
+ app.py
817
+ ├── init_db() [db/schema.py]
818
+ ├── build_tab_inspect() [ui/tab_inspect.py]
819
+ │ └── inspect_model()
820
+ │ ├── check_quantization() [core/fetcher.py]
821
+ │ ├── extract_config_params() [core/layer_profile.py]
822
+ │ ├── load_all_shard_headers() [core/fetcher.py]
823
+ │ │ ├── get_all_shard_files()
824
+ │ │ │ └── find_index_file()
825
+ │ │ └── read_safetensors_header()
826
+ │ ├── scan_model_structure() [core/layer_profile.py]
827
+ │ │ ├── classify_qkv_suffix()
828
+ │ │ ├── is_norm_key()
829
+ │ │ └── _infer_head_dim()
830
+ │ └── summarize_structure()
831
+
832
+ ├── build_tab_analyze() [ui/tab_analyze.py]
833
+ │ └── run_analysis()
834
+ │ ├── init_db() [db/schema.py]
835
+ │ ├── check_quantization() [core/fetcher.py]
836
+ │ ├── extract_config_params() [core/layer_profile.py]
837
+ │ ├── upsert_model() [db/writer.py]
838
+ │ ├── load_all_shard_headers() [core/fetcher.py]
839
+ │ ├── scan_model_structure() [core/layer_profile.py]
840
+ │ ├── upsert_component() [db/writer.py]
841
+ │ ├── get_analyzed_layers() [db/writer.py]
842
+ │ ├── load_tensor_remote() ×3 [core/fetcher.py]
843
+ │ ├── analyze_layer() [core/metrics.py]
844
+ │ │ ├── pearson()
845
+ │ │ ├── spearman_r()
846
+ │ │ ├── ssr()
847
+ │ │ ├── svr()
848
+ │ │ ├── cos_U()
849
+ │ │ ├── cos_V()
850
+ │ │ └── sigma_stats()
851
+ │ ├── write_layer_records() [db/writer.py]
852
+ │ │ └── infer_layer_type()
853
+ │ ├── update_model_summary() [db/writer.py]
854
+ │ │ └── _calc_summary_row()
855
+ │ └── summarize_records() [core/metrics.py]
856
+
857
+ ├── build_tab_leaderboard() [ui/tab_leaderboard.py]
858
+ │ └── load_leaderboard()
859
+ │ ├── init_db() [db/schema.py]
860
+ │ ├── get_leaderboard() [db/reader.py]
861
+ │ └── _format_leaderboard()
862
+
863
+ └── build_tab_database() [ui/tab_database.py]
864
+ ├── load_db_stats()
865
+ │ └── get_db_stats() [db/schema.py]
866
+ ├── load_model_list()
867
+ │ └── get_analyzed_models() [db/reader.py]
868
+ ├── load_model_detail()
869
+ │ ├── get_model_summary() [db/reader.py]
870
+ │ └── get_resume_status() [db/reader.py]
871
+ └── load_layer_data()
872
+ └── get_layer_metrics() [db/reader.py]
873
+ ```
874
+
875
+ ---
876
+
877
+ ## 9. 关键设计决策
878
+
879
+ ### 零 hard coding 原则
880
+ 任何模型相关的参数(head_dim、层数、组件结构)
881
+ 都从权重文件的 key 名自动推断,不写死任何模型名或层号。
882
+
883
+ ### GQA 支持
884
+ 当 `n_q_heads > n_kv_heads` 时(如 Llama-3-8B 的 32Q/8KV),
885
+ `group = n_q / n_kv`,每个KV head对应group个Q head,
886
+ 全部独立计算,每个Q head一条记录。
887
+
888
+ ### K=V 共享(Gemma全局层)
889
+ Gemma-4-31B 每6层有一个全局层,V权重不存在(K和V共享)。
890
+ 检测方式:V的key不在任何shard的header中。
891
+ 处理方式:`W_v = W_k.clone()`,KV相关指标设为理论值。
892
+ 存储方式:`kv_shared=1`,`layer_type="global"`。
893
+
894
+ ### 断点续传粒度
895
+ 以 `(model_id, prefix, layer)` 为粒度。
896
+ 某层的所有head全部写入才算完成。
897
+ 允许随时中断,下次从未完成的层继续。
898
+
899
+ ### 排行榜的 wang_score
900
+ 无论 `model_summary` 的 `layer_type` 是 all/standard/global,
901
+ `wang_score` 统一从 standard 层的 `ssr_QK` 计算,
902
+ 避免全局层(K=V共享,SSR=0)人为拉高评分。
903
+
904
+ ### 每个(model_id, prefix)在排行榜中是一行
905
+ 排行榜以 `(model_id, prefix)` 为单位,
906
+ 多模态模型(如Gemma-4)的language_model和vision_tower分别占一行。
907
+
908
+ ---
909
+
910
+ ## 10. 部署说明
911
+
912
+ ### HuggingFace Space 部署
913
+
914
+ 1. 创建 Space,选择 Gradio SDK
915
+ 2. 在 Space Settings 中添加 **Persistent Storage**(挂载到 `/data`)
916
+ - `wang_laws.db` 重启后不丢失
917
+ 3. 上传所有文件(保持目录结构)
918
+ 4. 如需访问私有模型,在 Space Secrets 中设置 `HF_TOKEN`
919
+
920
+ #### 配置管理员写入权限(重要)
921
+
922
+ 在 **Space Settings → Secrets** 中添加:
923
+
924
+ | Secret 名称 | 值 | 说明 |
925
+ | ------------- | ---------------- | ------------------------------- |
926
+ | `WRITE_TOKEN` | 你自己设置的密码 | 管理员写库密钥,不进入 git repo |
927
+
928
+ **工作原理:**
929
+ ```
930
+ HF Space Secrets(加密存储,不在 git 中)
931
+ ↓ HF 运行时自动注入
932
+ Docker 容器环境变量 WRITE_TOKEN
933
+ ↓ 服务端读取
934
+ os.environ.get("WRITE_TOKEN")
935
+ ↓ 与用户输入的 Admin Token 比对(纯服务端,前端不可见)
936
+ True → 写入数据库
937
+ False → 只读模式,分析正常运行
938
+ ```
939
+
940
+ **三类用户的体验:**
941
+
942
+ | 用户 | Admin Write Token | 行为 |
943
+ | --------------- | ----------------- | ---------------------------------- |
944
+ | 你(管理员) | 填写正确密钥 | 分析结果写入数据库,排行榜更新 |
945
+ | 审稿人 / 复现者 | 留空 | 分析正常运行,指标完整显示,不写库 |
946
+ | 恶意用户 | 随意填写 | 分析可以跑,写库被拒绝 |
947
+
948
+ **未配置 `WRITE_TOKEN` 时:**
949
+ ```python
950
+ # check_write_permission() 的行为:
951
+ server_token = os.environ.get("WRITE_TOKEN", "")
952
+ if not server_token:
953
+ return False # 服务端未配置 → 拒绝所有写入
954
+ ```
955
+ 即使有人猜到任意字符串也无法写入。
956
+
957
+ ### 本地运行
958
+
959
+ ```bash
960
+ pip install -r requirements.txt
961
+
962
+ # 可选:设置写入权限
963
+ export WRITE_TOKEN="your_secret_password"
964
+
965
+ python app.py
966
+ # 浏览器打开 http://127.0.0.1:7860
967
+ ```
968
+
969
+ 本地运行时数据库存于当前目录的 `wang_laws.db`。
970
+ 不设置 `WRITE_TOKEN` 则所有人都是只读模式。
971
+ ```
972
+
973
+ ---
974
+
975
+ ## 改动汇总
976
+
977
+ | 文件 | 改动 |
978
+ | ----------------------- | ---------------------------------------------------------------------------- |
979
+ | `db/writer.py` | 末尾追加 `check_write_permission()`,其余不变 |
980
+ | `ui/tab_analyze.py` | 完整重写:加 `admin_token` 参数,所有写库操作加 `can_write` 判断,日志改英文 |
981
+ | `README.md` | 第10节部署说明扩充写权限配置说明 |
982
+ | `db/schema.py` | 不变 |
983
+ | `db/reader.py` | 不变 |
984
+ | `ui/tab_inspect.py` | 不变 |
985
+ | `ui/tab_leaderboard.py` | 不变 |
986
+ | `ui/tab_database.py` | 不变 |
987
+ | `app.py` | 不变 |
988
+
989
+
990
+ ### 注意事项
991
+
992
+ - 分析大模型(如 70B)时每层需要约 30 秒(受 HF CDN 网速限制)
993
+ - HF Space 免费版有 48 小时超时限制,建议开启断点续传分批分析
994
+ - 量化模型(GPTQ/AWQ/GGUF)自动拒绝,需使用原始 BF16 版本
995
+
996
+ ---
997
+
998
+ ## 11. 依赖清单
999
+
1000
+ ```
1001
+ gradio>=4.0.0 # Web UI 框架
1002
+ requests # HTTP Range Request 读取远程权重
1003
+ numpy # 数值计算(统计汇总)
1004
+ scipy # spearman相关系数
1005
+ torch # SVD分解(torch.linalg.svd)
1006
+ huggingface_hub # list_repo_files(文件列表)
1007
+ ```
1008
+
1009
+ Python 内置(无需安装):
1010
+ ```
1011
+ sqlite3 # 数据库
1012
+ struct # 解析safetensors header的8字节整数
1013
+ json # 解析safetensors header JSON
1014
+ re # 正则提取层号
1015
+ datetime # 时间戳
1016
+ dataclasses # LayerProfile数据结构
1017
+ ```