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