math-under-llm / README.md
Alex W.
update README. include all 6 UI tabs
3b8a0de
---
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` | 重构 | 全文英文化 |