Spaces:
Running
Running
| 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` | 重构 | 全文英文化 | |