已合并
[Feature] msAgent Skills迁移msModelSlim仓 #907
joejoezhou创建于 26 天前
[Feature] msAgent Skills迁移msModelSlim仓 #907
已合并
共 102 个文件变更+9256-10
| @@ -34,6 +34,7 @@ msmodelslim/ | |||
| 34 | ├── msmodelslim/ # 源代码 | 34 | ├── msmodelslim/ # 源代码 |
| 35 | ├── precision_tool/ # 伪量化精度评估工具(V0框架) | 35 | ├── precision_tool/ # 伪量化精度评估工具(V0框架) |
| 36 | ├── security/ # 安全检查基础模块(V0框架) | 36 | ├── security/ # 安全检查基础模块(V0框架) |
| 37 | +├── skills/ # Agent Skills(资料管理、量化安装/适配/量化/策略/调优/测评等) | ||
| 37 | ├── test/ # 测试用例与测试脚本 | 38 | ├── test/ # 测试用例与测试脚本 |
| 38 | ├── install.sh # 安装脚本 | 39 | ├── install.sh # 安装脚本 |
| 39 | ├── requirements.txt # 第三方依赖列表 | 40 | ├── requirements.txt # 第三方依赖列表 |
| @@ -54,6 +55,7 @@ msmodelslim/ | |||
| 54 | ├── lab_calib/ # 量化校准数据集示例,如json、jsonl、图片等 | 55 | ├── lab_calib/ # 量化校准数据集示例,如json、jsonl、图片等 |
| 55 | ├── lab_practice/ # 量化最佳实践仓库,管理各模型已验证的量化配置 | 56 | ├── lab_practice/ # 量化最佳实践仓库,管理各模型已验证的量化配置 |
| 56 | ├── msmodelslim/ # 源代码 | 57 | ├── msmodelslim/ # 源代码 |
| 58 | +├── skills/ # Agent Skills(含量化安装/适配/量化/策略/调优/测评) | ||
| 57 | ├── test/ # 测试用例与测试脚本 | 59 | ├── test/ # 测试用例与测试脚本 |
| 58 | ├── install.sh # 安装脚本 | 60 | ├── install.sh # 安装脚本 |
| 59 | ├── requirements.txt # 第三方依赖列表 | 61 | ├── requirements.txt # 第三方依赖列表 |
| @@ -0,0 +1,134 @@ | |||
| 1 | +# msModelSlim Skills | ||
| 2 | + | ||
| 3 | +本目录存放可供 Agent 加载的 Skills,覆盖模型量化与精度调优的完整闭环:安装、适配、调优策略、量化执行、评测与端到端编排。 | ||
| 4 | + | ||
| 5 | +## 目录结构 | ||
| 6 | + | ||
| 7 | +| 领域 | 顶层 Skill | 说明 | 可委派子 Skill(供编排) | | ||
| 8 | +|---|---|---|---| | ||
| 9 | +| 端到端编排 | [tuning/](tuning/) | 全自动量化调优编排者(workflow) | — | | ||
| 10 | +| 安装与环境 | [installation/](installation/) | 安装与环境校验 | `installation`(自身) | | ||
| 11 | +| 模型适配 | [adaptation/](adaptation/) | 校准适配 + EP 并行适配 | `adaptation/calibration`、`adaptation/calibration/analyze`、`adaptation/calibration/dequant`、`adaptation/calibration/verify`、`adaptation/ep` | | ||
| 12 | +| 敏感层分析 | [sensitive-layer-analysis/](sensitive-layer-analysis/) | 敏感层分析执行 | `sensitive-layer-analysis`(自身) | | ||
| 13 | +| 调优策略 | [strategy/](strategy/) | 调优策略与 Practice 生成 | `strategy/practice-cfg`、`strategy/standing_high_with_experience/expert-rules` | | ||
| 14 | +| 量化执行 | [quantization/](quantization/) | 执行 `msmodelslim quant` | `quantization`(自身) | | ||
| 15 | +| 评测 | [evaluation/](evaluation/) | 评测配置 / 执行 / 评测集 herding 压缩 | `evaluation/evaluation-cfg`、`evaluation/evaluate`、`evaluation/dataset-compression-herding` | | ||
| 16 | +| 资料管理 | [docs-management/](docs-management/) | 资料管理 | — | | ||
| 17 | + | ||
| 18 | +调优闭环脚本共享库(无 SKILL):[tuning-loop-lib/](tuning-loop-lib/)。 | ||
| 19 | + | ||
| 20 | +## 调用方式 | ||
| 21 | + | ||
| 22 | +同一套 Skill 支持两种入口,共用同一批子 Skill 与脚本,差异仅在「由谁驱动」。 | ||
| 23 | + | ||
| 24 | +**1. 直接使用(独立完成任务)** | ||
| 25 | + | ||
| 26 | +用户针对单一需求(安装、写适配器、生成 Practice、执行量化、跑评测等)直接触发对应 Skill,由 Skill 按自身流程独立完成,不依赖 `tuning` 编排。 | ||
| 27 | + | ||
| 28 | +- 领域 Skill(`adaptation` / `strategy` / `evaluation`)是**领域入口**,内部路由到子 Skill 完成具体工作; | ||
| 29 | +- 单任务 Skill(`installation` / `quantization` / `sensitive-layer-analysis`)自身即可独立完成,无子级。 | ||
| 30 | + | ||
| 31 | +**2. 端到端调优(由 `tuning` 编排)** | ||
| 32 | + | ||
| 33 | +用户要求「自动量化调优」时,由 `tuning` 作为编排者接管,按「环境准备 → 模型准备 → 量化配置调优 → 结果输出」四阶段,委派各可委派子 Skill 协作完成: | ||
| 34 | + | ||
| 35 | +- 委派对象是带 `metadata.subagent` 声明的**可委派子 Skill**(见「subagent ↔ skill 绑定」); | ||
| 36 | +- 顶层领域 Skill 不直接参与委派,作为领域入口供直接使用; | ||
| 37 | +- 编排层与子 Skill 通过 SUBAGENT_IO v1 契约衔接(字段定义见 `tuning/references/`)。 | ||
| 38 | + | ||
| 39 | +## 内容组织 | ||
| 40 | + | ||
| 41 | +skills 文档遵循「docs 为权威、skill 只写增量」的分工,避免重复: | ||
| 42 | + | ||
| 43 | +1. **docs(权威)**:安装指南、CLI 参数、YAML schema、算法原理、模型接入等完整细节以 `docs/` 为准,skills 内只链接、不复写; | ||
| 44 | +2. **skill(增量)**:只保留执行所需的行为增量——触发条件、执行步骤、门禁与实战经验(docs 未覆盖项); | ||
| 45 | +3. **编排契约**:`tuning/references/` 只定义委派时机、input/output 字段与判定标准,指向各子 Skill,不重复实现细节。 | ||
| 46 | + | ||
| 47 | +## subagent ↔ skill 绑定 | ||
| 48 | + | ||
| 49 | +调优闭环中可作为 subagent 委派的 skill(如 `strategy/practice-cfg`、`adaptation/ep`、`quantization` 等),其绑定关系**内置于各自 SKILL.md 的 frontmatter**:`metadata.subagent` 声明 `id`(委派标识,即 `subagent_type`)与 `bind`(委派时须绑定加载的 skill 根目录列表,含自身)。绑定清单与解析规则见 [tuning/references/subagent_io_protocol.md](tuning/references/subagent_io_protocol.md)「subagent ↔ skill 绑定」章节。 | ||
| 50 | + | ||
| 51 | +部分被委派 subagent 可**再委派**其 `bind` 内的子 skill(嵌套委派,链深上限 2),如 `strategy/practice-cfg` 步骤②再委派 `sensitive-layer-analysis` 完成敏感层分析,主 Agent 无需感知;规则见该文档「子任务再委派」章节。 | ||
| 52 | + | ||
| 53 | +## Skill 编写规范 | ||
| 54 | + | ||
| 55 | +所有 `SKILL.md` 遵循统一结构与命名,保证**人类可读、Agent 可执行、经验可积累**。任何新增 / 修改 skill 均须满足本规范。 | ||
| 56 | + | ||
| 57 | +### 1. 标题与命名 | ||
| 58 | + | ||
| 59 | +- 一级标题统一为 `# <中文名>(<frontmatter name>)`,如 `# 敏感层分析(sensitive-layer-analysis)`;其中 name 必须与 frontmatter `name` 一致。 | ||
| 60 | +- 小节名一律使用中文;`## Overview`、`## Skill: xxx` 等英文标题统一改写为 `## 概述` 等中文小节。 | ||
| 61 | +- 同级小节标题保持同构:同为动作就同为动宾(如 `## 检查清单` 不混用 `## 清单检查`),同为门禁就同用规范门禁名(见下)。 | ||
| 62 | + | ||
| 63 | +### 2. 统一小节骨架 | ||
| 64 | + | ||
| 65 | +**执行 / 决策型 skill**(会被委派或独立执行完整任务的 skill)按需选用以下小节,顺序推荐为: | ||
| 66 | + | ||
| 67 | +```markdown | ||
| 68 | + | ||
| 69 | +## 职责(或 ## 概述) # 解决什么 / 不解决什么 / 权威参考指向 docs | ||
| 70 | + | ||
| 71 | +## 输入 / ## 输出 # 委派或直用契约的字段表(类型/必填/说明) | ||
| 72 | + | ||
| 73 | +## 流程 # 编号步骤(1. 2. 3. 或 ①②③),可用决策树/流程图画清分支 | ||
| 74 | + | ||
| 75 | +## 硬门禁 / ## 红线 / ## 检查清单 / ## 通过-失败标准 # 见「门禁范式」 | ||
| 76 | + | ||
| 77 | +## 常见错误 / ## 错误处理 # 本 skill 高频错误 → 处置 | ||
| 78 | + | ||
| 79 | +## 经验条目(Experiences) # 追加制登记表,见「经验条目规范」 | ||
| 80 | + | ||
| 81 | +## 参考资料 # 链接 references/ 与 docs/ | ||
| 82 | +``` | ||
| 83 | + | ||
| 84 | +**路由 / 入口型 skill**(`adaptation`、`evaluation`、`strategy`):只保留「子目录表 + 使用方式」即可,**不加**经验条目区(见经验条目适用层)。 | ||
| 85 | + | ||
| 86 | +### 3. 门禁范式(统一命名,不得使用异名) | ||
| 87 | + | ||
| 88 | +| 范式 | 语义 | 格式 | | ||
| 89 | +|------|------|------| | ||
| 90 | +| `## 硬门禁` | 流程**启动前**必须满足;不满足即停、**不得带病进入**后续步骤 | 条目化「必须…,否则…」 | | ||
| 91 | +| `## 红线` | **贯穿全流程**的禁止行为 / 边界 | `- 不得…` / `- 禁止…` 清单 | | ||
| 92 | +| `## 检查清单` | 执行前逐项自检 | `- [ ]` 勾选项 | | ||
| 93 | +| `## 通过/失败标准` | 判定本 skill 是否完成 | 显式「通过 = …;失败 = …」 | | ||
| 94 | +| `## 常见错误` / `## 错误处理` | 高频异常 → 处置 | 表格(错误 / 原因 / 修复) | | ||
| 95 | + | ||
| 96 | +- 同语义门禁不得再自造名称(如「硬性门禁」「红线和原则」须归入上表);正文内的「必须 / 禁止」措辞不受限。 | ||
| 97 | +- 门禁必须是**可判定**的:给出判定输入(exit code、文件存在性、schema 校验、数值门限等),不能只写"注意"。 | ||
| 98 | + | ||
| 99 | +### 4. 经验条目规范(显式、可追加) | ||
| 100 | + | ||
| 101 | +统一格式为一张**追加制登记表**,置于各 skill 的 `## 经验条目(Experiences)` 小节: | ||
| 102 | + | ||
| 103 | +```markdown | ||
| 104 | + | ||
| 105 | +## 经验条目(Experiences,追加制) | ||
| 106 | + | ||
| 107 | +> 追加规范:连续编号 `[E-序号]`(跨 skill 不要求全局唯一,按文件内递增);正文保留权威展开,本表只做索引 + 元数据登记(结论一句话);来源三选一:实测编号(F0-x / Dx / Tx)| 用户反馈 | 代码实证;验证状态三选一:已回归 | 待验证 | 已上流 docs。经验上流到 docs 后改为 `已上流 docs`,可在表内保留指针。 | ||
| 108 | + | ||
| 109 | +| 条目 | 主题 | 适用条件 / 触发信号 | 结论要点(一句话) | 正文位置 | 来源 | 验证状态 | | ||
| 110 | +|------|------|------|------|------|------|------| | ||
| 111 | +``` | ||
| 112 | + | ||
| 113 | +- **不记录日期/时间**:经验条目表不含日期列;时间戳对检索与复用无意义,勿追加。 | ||
| 114 | +- **经验条目 ≠ 常见错误**:常见错误表解决"出错怎么修";经验条目沉淀"正确认知 / 最佳做法 / 边界结论"(含其适用条件与验证状态)。 | ||
| 115 | +- **新增经验必须登记**:任何实测中发现、被用户反馈纠正、或经代码实证的结论,应追加为一条 `[E-序号]`,而非仅散落在正文。 | ||
| 116 | +- **淘汰与上流**:随版本过期的条目整行删除;被 docs 收录的经验改验证状态为 `已上流 docs` 并保留指针,遵循「docs 权威、skill 只写增量」原则。 | ||
| 117 | + | ||
| 118 | +### 5. 经验条目的适用层 | ||
| 119 | + | ||
| 120 | +| 适用层 | skill | 说明 | | ||
| 121 | +|------|------|------| | ||
| 122 | +| **强适用**(必须带经验条目区) | `sensitive-layer-analysis`、`strategy/practice-cfg`、`evaluation/evaluation-cfg`、`evaluation/evaluate`、`quantization`、`adaptation/calibration/analyze`、`adaptation/calibration/dequant`、`adaptation/calibration/verify`、`adaptation/calibration/layer_wise`、`evaluation/dataset-compression-herding` | 反复实操、每次运行都可能产出新经验(环境坑 / CLI 差异 / schema 边界 / 结构判定信号) | | ||
| 123 | +| **部分适用**(沉淀原则级条目) | `tuning`、`docs-management`、`adaptation/calibration`(主流程)、`adaptation/ep` | 只登记编排 / 文档管理 / 适配流程级原则;具体操作坑**下沉**给其子 skill 登记,避免编排层膨胀 | | ||
| 124 | +| **弱适用 / 不加** | `adaptation`、`evaluation`、`strategy`(路由入口) | 只做路由分发;如确有"哪类模型走哪条路径"的教训,登记到对应子 skill | | ||
| 125 | +| **经验库载体** | `strategy/standing_high_with_experience/expert-rules` | 全体系经验的 L1/L2/L3 检索索引与专家库,见其 SKILL.md「三级知识结构」;新模型个案按 L3 约定追加 `models/<vendor>/<model>.md` | | ||
| 126 | + | ||
| 127 | +### 6. 风格约定 | ||
| 128 | + | ||
| 129 | +- 语言以中文为主;代码、命令、CLI 参数、YAML 字段名保留原文(不加翻译)。 | ||
| 130 | +- 决策树 / 流程优先用文本图(text 代码块)或 Markdown 表格表达,少用无法在终端阅读的图形字符。 | ||
| 131 | +- "权威参考以 docs 为准"的内容一律用链接指向 `docs/`,skill 内不复写长文。 | ||
| 132 | +- **遵从资料规范公共校验 CE-05 / CE-06**(《[公共校验清单](../docs/zh/contributing/development_guide/docs_standards/00_common_checklist.md)》):引用 `docs/` 标题写作 `《[正式标题](相对路径)》`;中文语境数字与量词/单位不留空格(如 `前2层`、`≤2卡`)。 | ||
| 133 | +- **CE-04**:全文(不含 fenced code 内示例)仅一个一级标题。analyze 的「分析报告」是子 agent **落盘产物模板**(`{save_path}/model_analysis_report.md`),其 `# 分析报告` 仅出现在代码块示例中,不是本 skill 的第二 H1。 | ||
| 134 | +- 对用户 / 编排层的**必做沟通**(阻塞、需确认、风险提示)用「必须 / 不得」显式写出,并给出话术要点。 | ||
| @@ -0,0 +1,66 @@ | |||
| 1 | +--- | ||
| 2 | +name: adaptation | ||
| 3 | +description: | | ||
| 4 | + msModelSlim 模型适配总入口。适配准备由 4个子任务组成,每个子任务自带门禁,主 Agent 在收尾统一验收: | ||
| 5 | + ① analyze(分析,门禁:next_step 判定); | ||
| 6 | + ② calibration(写 adapter,门禁:verify 四步;含条件前置 dequant 反量化); | ||
| 7 | + ③ ep(多卡 MoE EP 并行改造,门禁:[EP_CHECK]+[EP_ACT_GATE],多卡才触发); | ||
| 8 | + ④ verify(主 Agent 适配验收,门禁:四步全 PASS = NPU 前向推理判定基准;兼作 calibration 内部门禁)。 | ||
| 9 | + 触发:新模型接入、写 adapter、适配器验证、FP8 反量化接入、逐层加载、MoE EP 并行适配等。 | ||
| 10 | +metadata: | ||
| 11 | + version: 0.2.0 | ||
| 12 | + domain: quant | ||
| 13 | + framework: msmodelslim | ||
| 14 | + skill_class: workflow | ||
| 15 | +--- | ||
| 16 | + | ||
| 17 | +# 模型适配(adaptation) | ||
| 18 | + | ||
| 19 | +## 是什么 | ||
| 20 | + | ||
| 21 | +「模型适配」= 让目标模型能在 msModelSlim 量化流程中被正确加载、遍历与量化导出。按适配目标分为两类: | ||
| 22 | + | ||
| 23 | +| 适配类型 | 目录 | 目标 | | ||
| 24 | +|---|---|---| | ||
| 25 | +| **校准适配**(Calibration Adaptation) | [calibration/](calibration/SKILL.md) | 把模型接入**校准量化**(PTQ / W8A8 / W4A16)流程:写 Model Adapter,使 `msmodelslim quant` 可加载、校准、量化、导出 | | ||
| 26 | +| **并行适配**(Parallel Adaptation) | [ep/](ep/SKILL.md) | 把 MoE 模型改造成**多卡 EP** 并行状态,保证后续量化全程跑在 EP 上 | | ||
| 27 | + | ||
| 28 | +> **命名澄清**:`calibration/` 是「校准适配」的缩写,指为**校准量化流程**做适配(写 Model Adapter),**不是**「跑校准集计算 scale 的那一步」。后者在量化执行时由 `msmodelslim quant` 内部完成,不在此目录。同理,`ep/` 指「EP 并行适配」,两者都是模型适配(`adaptation`)的子类。 | ||
| 29 | + | ||
| 30 | +## 使用方式 | ||
| 31 | + | ||
| 32 | +- **直接使用**:用户要求「接入新模型 / 写适配器 / 适配器验证 / FP8 反量化 / 逐层加载 / MoE EP 并行适配」时,按下方决策树与路由速查执行,由本 Skill 独立完成适配(含各子任务门禁),不依赖 `tuning`。 | ||
| 33 | +- **被 `tuning` 编排**:端到端调优在「模型准备」阶段委派本 Skill 的可委派子 Skill(`analyze` / `calibration` / `dequant` / `ep` / `verify`),委派时机与 input/output 契约见 `tuning/references/prepare_model.md`。本 Skill 是**领域入口**,自身不作为 subagent 被委派。 | ||
| 34 | + | ||
| 35 | +## 适配顺序(决策树) | ||
| 36 | + | ||
| 37 | +适配准备由 4个适配子任务构成;每个子任务完成时自带**门禁**,通过后才进入下一步;主 Agent 在适配准备收尾统一验收(触发 verify 门禁),**不跳步**: | ||
| 38 | + | ||
| 39 | +```text | ||
| 40 | +适配准备 | ||
| 41 | +├─ ① analyze(分析) 门禁: next_step 判定 | ||
| 42 | +├─ ② calibration(写适配器) 门禁: verify 四步(内部门禁,随 verification_steps 回传) | ||
| 43 | +│ └─ (条件) dequant(反量化) 门禁: adapter_updated=true → 回到 ② | ||
| 44 | +├─ ③ ep(EP 并行改造,多卡才触发) 门禁: [EP_CHECK] + [EP_ACT_GATE] | ||
| 45 | +└─ ④ verify(主 Agent 适配验收) 门禁: 四步全 PASS(NPU 前向推理判定基准) | ||
| 46 | +``` | ||
| 47 | + | ||
| 48 | +**门禁说明**: | ||
| 49 | + | ||
| 50 | +- **① analyze**:判断适配可行性(实现来源解析),输出风险结论与 `next_step`(`model-adapt` / `dequant` / `blocked` / `need_user_input`)。`blocked` / `need_user_input` 即停,回到本子 skill 修复或向用户索要材料,不得跳过继续。 | ||
| 51 | +- **② calibration**:写 Model Adapter(`handle_dataset` / `init_model` / `generate_model_visit` / `generate_model_forward` / `enable_kv_cache` + `config.ini` 注册 + `bash install.sh`)。完成时**自带 verify 四步作为内部门禁**,随 `verification_steps` 回传。 | ||
| 52 | +- **②' dequant(条件前置)**:仅当 ① 判定为原生量化模型(`next_step: dequant`)时,先执行反量化(FP8 per-block / per-channel → 写 `convert_*_to_bf16.py` 接入 adapter);门禁 `adapter_updated=true`,完成后**回到 ②** 继续写 Model Adapter;普通 FP 模型从 ① 直接到 ②。 | ||
| 53 | +- **③ ep(条件触发)**:仅当调优需多卡(≥2卡)且命中 EP 路由时走;MoE 检查 → EP 就绪检查/改造,门禁 `[EP_CHECK]` + `[EP_ACT_GATE]`;非 MoE / 单卡跳过。 | ||
| 54 | +- **④ verify(主 Agent 适配验收)**:适配准备收尾的统一验收,也是「NPU 前向推理」判定基准——若 ② 的内部门禁缺失或未全过,主 Agent 独立下发 ④,四步全 PASS(Step2 全回退量化 + Step3 权重一致性/可加载保存)才视为模型可在目标设备正常前向推理。 | ||
| 55 | +- **可选高阶 layer_wise**:逐层量化(CPU 内存不足或用户明确要求时)依赖 ② 完成后启用,不占主链路。 | ||
| 56 | + | ||
| 57 | +> **verify 双重角色**:既是 ② calibration 的内部门禁(随 `verification_steps` 回传),又是 ④ 主 Agent 的适配验收门禁(独立下发,不得另造命令代替)。两层共用同一套四步验证。 | ||
| 58 | + | ||
| 59 | +## 路由速查 | ||
| 60 | + | ||
| 61 | +| 用户诉求 | 入口 | | ||
| 62 | +|---|---| | ||
| 63 | +| 新模型接入 / 写 adapter / FP8 反量化 / 逐层加载 | `calibration/`(必要时先 `analyze/`) | | ||
| 64 | +| 适配器验证 / 主 Agent 适配验收(四步验证) | `verify/` | | ||
| 65 | +| MoE 多卡 EP 并行 / EP 就绪检查 | `ep/` | | ||
| 66 | +| 调优多卡 MoE:先校准再 EP | 先 `calibration/` 完成校准适配(含四步验证),再 `ep/` | | ||
| @@ -0,0 +1,120 @@ | |||
| 1 | +--- | ||
| 2 | +name: calibration | ||
| 3 | +description: | ||
| 4 | + 为 msModelSlim 创建基础 Transformers 模型适配器(Model Adapter)。 | ||
| 5 | + 包含创建适配器、实现必需接口与注册安装流程。 | ||
| 6 | + 适用:Decoder-only LLM、理解类 VLM(仅 LLM/text 部分)。 | ||
| 7 | + 不适用:多模态生成模型(图像/视频/语音生成)、Encoder-only、非 Transformers 架构。 | ||
| 8 | +metadata: | ||
| 9 | + # subagent 绑定声明:id = 委派标识(subagent_type);bind = 委派时须绑定加载的 skill 根目录(含 references/、scripts/) | ||
| 10 | + subagent: | ||
| 11 | + id: "adaptation/calibration" | ||
| 12 | + bind: | ||
| 13 | + | ||
| 14 | + - "adaptation/calibration" | ||
| 15 | + | ||
| 16 | +--- | ||
| 17 | + | ||
| 18 | +# 校准适配(calibration) | ||
| 19 | + | ||
| 20 | +本 Skill 是模型适配(`adaptation`)中的**校准适配**环节:指导如何为新模型创建基础适配器,使其跑通 W8A8/W4A16 校准量化流程。它是「模型适配」的一个子类,另一子类(EP 并行适配)见 [`adaptation/ep`](../ep/SKILL.md);适配顺序见 [`adaptation` 决策树](../SKILL.md)。 | ||
| 21 | + | ||
| 22 | +资料:《[ModelSlimPipelineInterfaceV1 / PTQ](../../../docs/zh/knowledge_base/ptq/README.md)》、《[LLM 接入](../../../docs/zh/knowledge_base/model/integrating_models.md)》。 | ||
| 23 | + | ||
| 24 | +> 说明:逐层量化(按层加载/懒加载)属于高阶可选特性,不是基础适配必需项。 | ||
| 25 | +> 仅当 CPU 内存无法全量加载权重,或用户明确要求时,再在基础适配和四步验证(由 `adaptation/calibration/verify` 执行)完成后启用。 | ||
| 26 | + | ||
| 27 | +## 适用范围 | ||
| 28 | + | ||
| 29 | +- **支持**:Decoder-only LLM、理解类 VLM(只处理文本/LLM 主干) | ||
| 30 | +- **不支持**:多模态生成(如 Stable Diffusion/Flux/Wan)、Encoder-only、非 Transformers | ||
| 31 | + | ||
| 32 | +## 核心工作流 | ||
| 33 | + | ||
| 34 | +### 1. 权重来源确认(新增必做提示) | ||
| 35 | + | ||
| 36 | +- **先询问并优先使用用户自有权重**:要求用户先提供本地模型权重路径(或已下载模型目录)。 | ||
| 37 | +- **仅在用户确认“没有权重”时再下载**:再协助用户执行下载流程,不要默认直接下载。 | ||
| 38 | +- **下载建议**: | ||
| 39 | + - 若先做结构分析,可先下载非权重文件:`modelscope download --model <org>/<model> --local_dir ./models/<name> --exclude '*.safetensors'` | ||
| 40 | + - 若进入完整量化/验证流程,需补齐可用权重文件。 | ||
| 41 | + | ||
| 42 | +### 2. 准备工作 | ||
| 43 | + | ||
| 44 | +- **分析模型**:阅读 `config.json` 与 `modeling_*.py`,确认结构与实现。 | ||
| 45 | + - 详见:[模型结构分析指南](references/model_analysis.md) | ||
| 46 | + | ||
| 47 | +### 3. 创建适配器 | ||
| 48 | + | ||
| 49 | +- **使用模板**: | ||
| 50 | + - LLM: `llm/model_adapter_template.py` | ||
| 51 | + - VLM: `vlm/vlm_model_adapter_template.py` | ||
| 52 | +- **实现接口**:实现 `handle_dataset`, `init_model`, `generate_model_visit`, `generate_model_forward`, `enable_kv_cache`。 | ||
| 53 | +- **关键原则**: | ||
| 54 | + - `visit` 与 `forward` 必须严格一致。 | ||
| 55 | + - MoE 模型建议 unpack 为纯线性层。 | ||
| 56 | + - 若原始模型存在需要保留的 buffer 权重,需在适配器中将其转换为 `nn.Parameter`;否则量化导出阶段通常不会保存 buffer 权重。 | ||
| 57 | + - **Tokenizer pad_token 兼容性必查**:若 `tokenizer.pad_token` / `pad_token_id` 为 `None`,必须在适配器中重写 `_load_tokenizer`,将 `pad_token` 回退到 `eos_token`,避免量化过程中在 `padding=True` 时直接报错。 | ||
| 58 | + - 常见报错: | ||
| 59 | + - `ValueError: Asking to pad but the tokenizer does not have a padding token.` | ||
| 60 | + - 根因链路: | ||
| 61 | + - `adapter.handle_dataset(...) -> _get_tokenized_data(...) -> tokenizer(..., padding=True, ...)` | ||
| 62 | + - 某些模型(如 MiniMax 系列)原生 tokenizer 未设置 `pad_token`。 | ||
| 63 | + - 推荐修复模板: | ||
| 64 | + | ||
| 65 | + ```python | ||
| 66 | + def _load_tokenizer(self, trust_remote_code=False): | ||
| 67 | + """Ensure tokenizer has a pad token for quantization dataset padding.""" | ||
| 68 | + tokenizer = super()._load_tokenizer(trust_remote_code=trust_remote_code) | ||
| 69 | + if tokenizer.pad_token is None: | ||
| 70 | + tokenizer.pad_token = tokenizer.eos_token | ||
| 71 | + return tokenizer | ||
| 72 | + ``` | ||
| 73 | + | ||
| 74 | + - 说明:优先使用 `eos_token` 作为回退;若目标模型有更合适的专用 pad token,可按模型官方约定替换。 | ||
| 75 | + - 详见:[适配器实现指南](references/implementation_guide.md) | ||
| 76 | + | ||
| 77 | +### 4. 注册与安装 | ||
| 78 | + | ||
| 79 | +- 在 `config/config.ini` 注册模型与入口,并执行 `bash install.sh` 安装msModelSlim。 | ||
| 80 | +- 详见:[适配器注册指南](references/registration_guide.md) | ||
| 81 | + | ||
| 82 | +### 5. 功能性验证(独立 Skill) | ||
| 83 | + | ||
| 84 | +- 适配器开发完成后,告知用户可自动执行功能性验证。 | ||
| 85 | +- 验证流程已独立为:`adaptation/calibration/verify`。 | ||
| 86 | +- 该验证 Skill 会自动按四步执行:结构覆盖减层测试模型 -> 全回退量化 -> 权重一致性与可加载/保存验证 -> 实际量化与描述文件规则校验。 | ||
| 87 | +- **验证语义**:减层覆盖全部结构类型即可(非全量权重);详见 verify skill「验证语义(硬约定)」。 | ||
| 88 | + | ||
| 89 | +### 6. 可选高阶特性:逐层量化 | ||
| 90 | + | ||
| 91 | +- 触发时机: | ||
| 92 | + - CPU 内存无法全量加载模型权重。 | ||
| 93 | + - 用户明确要求“逐层量化/逐层加载/懒加载/按层加载”。 | ||
| 94 | +- 启用顺序: | ||
| 95 | + - 必须先完成基础适配与四步验证,再进入逐层量化改造。 | ||
| 96 | +- 实现与验证指引: | ||
| 97 | + - 详见独立 Skill:`adaptation/calibration/layer_wise` | ||
| 98 | + | ||
| 99 | +## 参考资料 | ||
| 100 | + | ||
| 101 | +- 权威文档:《[ModelSlimPipelineInterfaceV1 / PTQ](../../../docs/zh/knowledge_base/ptq/README.md)》、《[LLM 大模型接入指南](../../../docs/zh/knowledge_base/model/integrating_models.md)》、《[LLM 量化集成指南](../../../docs/zh/knowledge_base/ptq/llm/integration_guide_large_language_model_quantization.md)》 | ||
| 102 | +- [模型结构分析指南](references/model_analysis.md) | ||
| 103 | +- [适配器实现指南](references/implementation_guide.md) | ||
| 104 | +- [适配器注册指南](references/registration_guide.md) | ||
| 105 | +- [接口检查清单](references/interface_checklist.md) | ||
| 106 | +- [核心工作流](references/core_workflow.md) | ||
| 107 | + | ||
| 108 | +> 上表前两项 docs 文档为适配流程与接口的权威来源;`references/` 下文件仅记录 docs 未覆盖的实战经验(pad_token 兼容、buffer 转 Parameter、MoE unpack 建议等)。 | ||
| 109 | + | ||
| 110 | +## 经验条目(Experiences,追加制) | ||
| 111 | + | ||
| 112 | +> 追加规范见 `skills/README.md`「经验条目」。连续编号 `[E-序号]`;正文保留权威展开,本表只做索引 + 元数据登记;来源:实测编号(F0-x / Dx / Tx)| 用户反馈 | 代码实证;验证状态:已回归 | 待验证 | 已上流 docs。本 skill 为**主流程**,仅登记适配流程级原则;具体子任务的操作级经验登记在各子 skill(analyze/dequant/verify/layer_wise)。 | ||
| 113 | + | ||
| 114 | +| 条目 | 主题 | 适用条件 / 触发信号 | 结论要点(一句话) | 正文位置 | 来源 | 验证状态 | | ||
| 115 | +|------|------|------|------|------|------|------| | ||
| 116 | +| E-001 | 权重来源先问后下 | 开始适配前 | 先询问并优先使用用户自有权重路径;仅在用户确认"没有权重"时才协助下载;先做结构分析可 `--exclude '*.safetensors'` 只下非权重文件 | 1. 权重来源确认 | 用户反馈 | 已回归 | | ||
| 117 | +| E-002 | pad_token 兼容必查 | 新模型 tokenizer 无 pad | `pad_token`/`pad_token_id` 为 `None` 时必须在 adapter 重写 `_load_tokenizer` 回退到 `eos_token`,否则 `padding=True` 报 `ValueError: Asking to pad...` | 3. 创建适配器 | 实测 | 已回归 | | ||
| 118 | +| E-003 | buffer 权重转 Parameter | 模型含需保留的 buffer | 适配器将关键 buffer 转为 `nn.Parameter`;否则量化导出阶段通常不保存 buffer 权重,导致产物缺键 | 3. 创建适配器 | 代码实证 | 已回归 | | ||
| 119 | +| E-004 | MoE unpack 与 visit/forward 一致 | MoE 模型适配 | MoE 建议 unpack 为纯线性层;`visit` 与 `forward` 必须严格一致(防遍历与计算错位) | 3. 创建适配器 | 代码实证 | 已回归 | | ||
| 120 | +| E-005 | 高阶特性后置 | 用户要求逐层量化时 | 逐层量化(layer_wise)必须建立在基础适配 + 四步验证完成后,不占主链路;仅 CPU 内存无法全量加载或用户明确要求时启用 | 6. 可选高阶特性 | 用户反馈 | 已回归 | | ||
| @@ -0,0 +1,189 @@ | |||
| 1 | +--- | ||
| 2 | +name: analyze | ||
| 3 | +description: 在实现适配器前对候选模型做分析。确定模型实现来源(transformers 或模型目录)、结构特征、内存约束下的逐层量化建议(可选)及 MoE 融合权重风险。适用于用户询问模型适配可行性或做适配前分析时使用。 | ||
| 4 | +metadata: | ||
| 5 | + # subagent 绑定声明:id = 委派标识(subagent_type);bind = 委派时须绑定加载的 skill 根目录(含 references/、scripts/) | ||
| 6 | + subagent: | ||
| 7 | + id: "adaptation/calibration/analyze" | ||
| 8 | + bind: | ||
| 9 | + | ||
| 10 | + - "adaptation/calibration/analyze" | ||
| 11 | + | ||
| 12 | +--- | ||
| 13 | + | ||
| 14 | +# 模型分析(analyze) | ||
| 15 | + | ||
| 16 | +## 适用范围 | ||
| 17 | + | ||
| 18 | +- 支持: | ||
| 19 | + - Decoder-only LLM | ||
| 20 | + - VLM 文本主干分析(仅 LLM/文本路径) | ||
| 21 | +- 不支持: | ||
| 22 | + - 既非 transformers 也非模型目录内 `modeling_*.py` 的实现 | ||
| 23 | + - 多模态生成模型(图像/视频/语音生成) | ||
| 24 | + | ||
| 25 | +## 输入 | ||
| 26 | + | ||
| 27 | +- 模型路径或模型仓库标识 | ||
| 28 | +- `config.json` | ||
| 29 | +- 可选:模型目录内 `modeling_*.py`、`model.safetensors.index.json` | ||
| 30 | +- 若本地缺少上述文件,先补齐输入: | ||
| 31 | + - 使用 `modelscope download --model <org>/<model> --local_dir ./models/<name> --exclude '*.safetensors'` 下载非权重文件。 | ||
| 32 | + - 在下载目录中读取 `config.json` 与 `modeling_*.py`,作为后续分析输入。 | ||
| 33 | + | ||
| 34 | +## 硬门禁:先解析实现来源 | ||
| 35 | + | ||
| 36 | +在进行任何结构分析前必须完成。Agent 应按以下步骤手动解析,无需脚本: | ||
| 37 | + | ||
| 38 | +1. **读取 `config.json`**: | ||
| 39 | + - 获取 `model_type` | ||
| 40 | + - 获取 `auto_map`(如有) | ||
| 41 | + | ||
| 42 | +2. **尝试从 transformers 解析实现**: | ||
| 43 | + - 检查 `transformers` 库是否支持该 `model_type`。 | ||
| 44 | + - 检查路径:`transformers/models/<model_type>/modeling_<model_type>.py` 是否存在。 | ||
| 45 | + - 如果存在,记录为 `transformers` 实现。 | ||
| 46 | + | ||
| 47 | +3. **若未解析到,尝试模型目录内实现**: | ||
| 48 | + - 检查 `auto_map` 指向的文件是否存在于模型目录。 | ||
| 49 | + - 检查模型目录内是否有 `modeling_*.py` 文件。 | ||
| 50 | + - 如果存在,记录为 `model-local` 实现。 | ||
| 51 | + | ||
| 52 | +4. **若两种路径均不可用**: | ||
| 53 | + - 停止分析。 | ||
| 54 | + - 要求用户提供可读的模型实现代码。 | ||
| 55 | + | ||
| 56 | +## 最小工作流 | ||
| 57 | + | ||
| 58 | +1. 解析实现来源(完成上述硬门禁)。 | ||
| 59 | +2. 判断模型类型、结构差异与连接关系: | ||
| 60 | + - 类型:纯 LLM / 多模态理解模型 / 多模态生成模型 | ||
| 61 | + - 对比常见 Qwen2类 LLM,记录特殊结构设计(如 MoE、非标准 attention、SSM/混合块、额外头部或并行分支) | ||
| 62 | + - 检查特殊结构连接关系(接入位置、前后依赖、串联/并联/残差连接、是否影响主干遍历) | ||
| 63 | +3. 识别结构特征: | ||
| 64 | + - decoder 层类、attention/MLP 模块命名、forward 签名 | ||
| 65 | + - **供 verify 减层覆盖**:记录结构标签种类及各自首次出现的层下标(如 `attn:linear_attention+ffn:dense @0`、`attn:full_attention+ffn:dense @3`、`attn:default+ffn:moe @3`);verify Step1 用「覆盖完备最小前缀」,不是全量、也不是盲目前 2层 | ||
| 66 | +4. 确定影响适配的特征: | ||
| 67 | + - 层遍历路径与顺序 | ||
| 68 | + - 是否存在内存约束导致需要逐层量化(可选高阶特性) | ||
| 69 | + - MoE 融合专家权重风险 | ||
| 70 | + - 量化模型反量化脚本风险 | ||
| 71 | + - MTP 结构实现可得性与权重处理风险 | ||
| 72 | +5. 产出结构化分析结果(参考下方模板)。 | ||
| 73 | +6. 给出后续动作: | ||
| 74 | + - 若判定为原生量化模型,优先进入 `adaptation/calibration/dequant` | ||
| 75 | + - 进入 `adaptation/calibration` | ||
| 76 | + - 或阻塞并说明需用户提供的内容 | ||
| 77 | + | ||
| 78 | +### 模型类型、结构差异与连接关系判定(相对常见 Qwen2) | ||
| 79 | + | ||
| 80 | +- `纯 LLM`:仅文本 token 输入,主干为 decoder-only 语言模型。 | ||
| 81 | +- `多模态理解模型`:含视觉/音频等编码器,但生成路径以文本主干为核心,允许仅分析并适配文本部分。 | ||
| 82 | +- `多模态生成模型`:核心目标是图像/视频/语音生成,当前流程不支持,应直接阻塞并说明原因。 | ||
| 83 | +- 结构差异只需记录“是否存在 + 影响方向”,不要求深入实现细节。 | ||
| 84 | +- 连接关系至少记录:特殊结构位于主干的哪个阶段、与哪些模块相连、连接方式(串联/并联/残差)及对遍历/forward 对齐的影响。 | ||
| 85 | + | ||
| 86 | +### MoE 布局判定 | ||
| 87 | + | ||
| 88 | +- `MoE 非融合`:专家按模块/列表展开(常见为每个 expert 各自持有 `gate/up/down` 线性层)。 | ||
| 89 | +- `MoE 融合`:多个 expert 的权重被打包为张量参数,不再是一组独立线性层。 | ||
| 90 | +- 若看到 `gate/up/down` 中任一或多项以 `[..., num_experts, ...]` 或 `[num_experts, ...]` 形式存储,应按“融合”处理。 | ||
| 91 | +- 对三维专家权重(如 gate/up/down 分别融合成 3D 参数)统一归类为 `MoE 融合`,并在报告中标注“可能需要 unpack”。 | ||
| 92 | + | ||
| 93 | +## 输出:分析报告 | ||
| 94 | + | ||
| 95 | +Agent 应直接生成分析报告(Markdown 格式),内容必须包含以下要素。请参考下方模板: | ||
| 96 | + | ||
| 97 | +> **产物路径约定**:报告以 Markdown 写入 `{save_path}/model_analysis_report.md`(与 `tuning/references/prepare_model.md` 委派契约、`adaptation/calibration/dequant` 的 `analysis_report_path` 字段一致)。未写盘即视为未完成输出。 | ||
| 98 | + | ||
| 99 | +```markdown | ||
| 100 | + | ||
| 101 | +# 分析报告 | ||
| 102 | + | ||
| 103 | +## 模型标识 | ||
| 104 | +- 模型路径/仓库:{model_path} | ||
| 105 | +- `model_type`:{model_type} | ||
| 106 | +- `architectures`:{architectures} | ||
| 107 | + | ||
| 108 | +## 实现来源解析 | ||
| 109 | +- 结果:`transformers` | `model-local` | `unsupported` | ||
| 110 | +- 依据: | ||
| 111 | + - 解析到的文件路径:{path} | ||
| 112 | + - 相关配置字段(`model_type`、`auto_map`):{details} | ||
| 113 | + | ||
| 114 | +## 模型特征与规格 | ||
| 115 | +- Hidden size:{hidden_size} | ||
| 116 | +- 层数:{num_layers} | ||
| 117 | +- Attention heads / KV heads:{num_heads} / {num_kv_heads} | ||
| 118 | +- 是否仅分析 VLM 文本部分:是/否 | ||
| 119 | + | ||
| 120 | +## 模型类型、结构差异与连接关系 | ||
| 121 | +- 模型类型:纯 LLM | 多模态理解模型 | 多模态生成模型 | ||
| 122 | +- 相对常见 Qwen2 的特殊结构:{special_structures} | ||
| 123 | +- 特殊结构连接关系:{special_structure_connections} | ||
| 124 | +- 对适配流程的影响:{structure_impact} | ||
| 125 | + | ||
| 126 | +## 逐层量化建议(可选高阶特性) | ||
| 127 | +- 是否建议逐层量化:是/否 | ||
| 128 | +- 触发原因(如 CPU 内存无法全量加载权重):{reason} | ||
| 129 | +- 约束(内存/运行环境):{constraints} | ||
| 130 | +- 说明:逐层量化不是基础适配必需项,建议在基础适配与四步验证完成后再进入 `adaptation/calibration/layer_wise` | ||
| 131 | + | ||
| 132 | +## MoE 评估 | ||
| 133 | +- 是否含 MoE:是/否 | ||
| 134 | +- 布局类型:无 MoE | MoE 非融合 | MoE 融合 | ||
| 135 | +- 疑似融合的键/模块:{keys} | ||
| 136 | +- 专家权重形态:独立线性层 | 打包张量(含 3D 专家权重) | ||
| 137 | +- 是否需要 unpack:是/否 | ||
| 138 | + | ||
| 139 | +## 适配影响要点 | ||
| 140 | +- Decoder 遍历路径:{traversal_path} | ||
| 141 | +- Attention 模块命名:{attn_module} | ||
| 142 | +- MLP 模块命名:{mlp_module} | ||
| 143 | +- `visit/forward` 严格对齐点:{alignment_points} | ||
| 144 | + | ||
| 145 | +## 量化与 MTP 风险评估 | ||
| 146 | +- 模型是否已量化:是/否 | ||
| 147 | +- 量化判定依据:{quant_evidence} | ||
| 148 | +- 是否已提供反量化脚本:是/否 | ||
| 149 | +- 反量化脚本状态说明:{dequant_status} | ||
| 150 | +- 是否存在 MTP 结构:是/否 | ||
| 151 | +- MTP 实现代码可获取性:可获取/不可获取 | ||
| 152 | +- MTP 风险说明:{mtp_risk} | ||
| 153 | + | ||
| 154 | +## 风险与后续动作 | ||
| 155 | +- 风险等级:低 | 中 | 高 | ||
| 156 | +- 阻塞项:{blockers} | ||
| 157 | +- 建议下一步: | ||
| 158 | + - 进入 `adaptation/calibration` | ||
| 159 | + - 或要求用户提供实现代码 | ||
| 160 | +``` | ||
| 161 | + | ||
| 162 | +### 风险识别与用户沟通要求(必须执行) | ||
| 163 | + | ||
| 164 | +- 若识别为“原生量化模型”(模型权重本身已量化),必须先调用 `adaptation/calibration/dequant` skill 再继续后续适配流程。 | ||
| 165 | +- 若识别为“模型本身已量化”,必须将“缺少反量化脚本”标记为阻塞项,并明确要求用户主动提供反量化脚本后再继续适配。 | ||
| 166 | +- 若识别到存在 MTP 结构但无法获取其实现代码,必须明确告知: | ||
| 167 | + - Agent 可能无法完整实现 MTP 结构适配; | ||
| 168 | + - 如需继续,用户需要自行复制 MTP 相关权重(按用户侧已有实现进行映射)。 | ||
| 169 | +- 上述两类风险至少有一项命中时,`风险等级` 不得低于“中”。 | ||
| 170 | + | ||
| 171 | +## 通过/失败标准 | ||
| 172 | + | ||
| 173 | +- 通过:实现来源为 `transformers` 或 `model-local`,模型类型为纯 LLM 或多模态理解模型,且报告完整;若命中量化/MTP 风险,已在报告中给出明确用户动作要求。 | ||
| 174 | +- 失败:来源未解析、为不支持的实现类型、判定为多模态生成模型,或命中“量化模型但无反量化脚本”阻塞条件。 | ||
| 175 | + | ||
| 176 | +## 参考 | ||
| 177 | + | ||
| 178 | +- [分析检查清单](references/analysis_checklist.md) | ||
| 179 | + | ||
| 180 | +## 经验条目(Experiences,追加制) | ||
| 181 | + | ||
| 182 | +> 追加规范见 `skills/README.md`「经验条目」。连续编号 `[E-序号]`;正文保留权威展开,本表只做索引 + 元数据登记;来源:实测编号(F0-x / Dx / Tx)| 用户反馈 | 代码实证;验证状态:已回归 | 待验证 | 已上流 docs。 | ||
| 183 | + | ||
| 184 | +| 条目 | 主题 | 适用条件 / 触发信号 | 结论要点(一句话) | 正文位置 | 来源 | 验证状态 | | ||
| 185 | +|------|------|------|------|------|------|------| | ||
| 186 | +| E-001 | 实现来源解析顺序 | 任何结构分析前 | 先查 transformers 是否支持该 `model_type`,再查模型目录 `auto_map` / `modeling_*.py`(model-local),两者均不可用才要求用户提供实现代码 | 硬门禁:先解析实现来源 | 代码实证 | 已回归 | | ||
| 187 | +| E-002 | MoE 融合判定 | 含 MoE 的模型 | `gate/up/down` 以 `[..., num_experts, ...]` 或 3D 参数存储即按「融合」处理,报告标注"可能需要 unpack";非融合=专家各自持有独立线性层 | MoE 布局判定 | 代码实证 | 已回归 | | ||
| 188 | +| E-003 | 原生量化模型分流 | 权重本身已量化 | 识别为原生量化模型必须先走 `adaptation/calibration/dequant`;缺反量化脚本=阻塞项,须要求用户提供;命中量化/MTP 风险时风险等级不得低于"中" | 风险识别与用户沟通要求 | 用户反馈 | 已回归 | | ||
| 189 | +| E-004 | 结构标签交 verify | 产出分析报告时 | 须写出各结构标签首次层下标,供 verify「减层结构覆盖」用;verify 非全量、禁止盲目前 N 层 | 流程 步骤3 | 用户反馈 | 待验证 | | ||
| @@ -0,0 +1,60 @@ | |||
| 1 | +# 分析检查清单 | ||
| 2 | + | ||
| 3 | +在开始实现适配器前使用本清单。 | ||
| 4 | + | ||
| 5 | +## 1)实现来源 | ||
| 6 | + | ||
| 7 | +- [ ] 读取 `config.json` | ||
| 8 | +- [ ] 仅解析一种来源: | ||
| 9 | + - [ ] `transformers/models/<model_type>/modeling_<model_type>.py` | ||
| 10 | + - [ ] 通过 `auto_map` 定位模型目录内 `modeling_*.py` | ||
| 11 | +- [ ] 若无法解析,停止并要求用户提供实现代码 | ||
| 12 | + | ||
| 13 | +## 2)模型类型、结构差异与连接关系(相对常见 Qwen2) | ||
| 14 | + | ||
| 15 | +- [ ] 判定模型类型:纯 LLM / 多模态理解模型 / 多模态生成模型 | ||
| 16 | +- [ ] 若为多模态理解模型,确认仅分析文本主干范围 | ||
| 17 | +- [ ] 若为多模态生成模型,标记为不支持并停止后续适配分析 | ||
| 18 | +- [ ] 记录相对常见 Qwen2 的特殊结构设计及可能影响 | ||
| 19 | +- [ ] 记录特殊结构连接关系(接入位置、前后依赖、串联/并联/残差)及其对遍历/forward 的影响 | ||
| 20 | + | ||
| 21 | +## 3)结构特征 | ||
| 22 | + | ||
| 23 | +- [ ] 确认 decoder 层类 | ||
| 24 | +- [ ] 确认 attention 与 MLP 模块命名 | ||
| 25 | +- [ ] 确认 forward 签名与关键返回值 | ||
| 26 | +- [ ] 确认用于遍历的层容器路径 | ||
| 27 | + | ||
| 28 | +## 4)逐层量化建议(可选高阶特性) | ||
| 29 | + | ||
| 30 | +- [ ] 评估全量加载内存与当前环境 | ||
| 31 | +- [ ] 决定是否建议逐层量化(按层加载) | ||
| 32 | +- [ ] 记录理由与预期影响 | ||
| 33 | +- [ ] 若建议启用,备注:先完成基础适配与四步验证,再进入 `adaptation/calibration/layer_wise` | ||
| 34 | + | ||
| 35 | +## 5)MoE 融合风险 | ||
| 36 | + | ||
| 37 | +- [ ] 判断模型是否含 MoE | ||
| 38 | +- [ ] 若含 MoE,检查专家权重布局(独立线性层 vs 打包张量) | ||
| 39 | +- [ ] 检查 `gate/up/down` 是否存在按 expert 维度打包的 3D 权重 | ||
| 40 | +- [ ] 标记为:无 MoE / MoE 非融合 / MoE 融合 | ||
| 41 | +- [ ] 记录可能的 unpack 需求 | ||
| 42 | + | ||
| 43 | +## 6)适配影响要点 | ||
| 44 | + | ||
| 45 | +- [ ] 记录 `generate_model_visit` 的遍历顺序预期 | ||
| 46 | +- [ ] 记录 `generate_model_forward` 的对齐约束 | ||
| 47 | +- [ ] 列出可能影响权重一致性检查的风险模块 | ||
| 48 | + | ||
| 49 | +## 7)量化模型风险(反量化脚本) | ||
| 50 | + | ||
| 51 | +- [ ] 判断模型是否“本身已量化” | ||
| 52 | +- [ ] 若已量化,要求用户主动提供反量化脚本 | ||
| 53 | +- [ ] 若用户未提供反量化脚本,标记为阻塞并停止后续适配实现 | ||
| 54 | + | ||
| 55 | +## 8)MTP 结构风险(实现缺失) | ||
| 56 | + | ||
| 57 | +- [ ] 判断模型是否包含 MTP 结构 | ||
| 58 | +- [ ] 若存在 MTP,检查是否能获取 MTP 实现代码 | ||
| 59 | +- [ ] 若无法获取实现代码,明确告知 agent 可能无法完整实现 MTP 结构适配 | ||
| 60 | +- [ ] 若用户仍需继续,提示用户需自行复制 MTP 权重 | ||
| @@ -0,0 +1,91 @@ | |||
| 1 | +#!/usr/bin/env python | ||
| 2 | +# -*- coding: UTF-8 -*- | ||
| 3 | +""" | ||
| 4 | +Adapter-side unpack example for MoE fused weights. | ||
| 5 | + | ||
| 6 | +Goal: | ||
| 7 | +- Detect whether experts are packed 3D tensors. | ||
| 8 | +- Unpack packed weights into per-expert Linear parameter keys. | ||
| 9 | +- Replace original MoE block with unpacked Linear-expert module. | ||
| 10 | +""" | ||
| 11 | + | ||
| 12 | +from typing import Dict | ||
| 13 | + | ||
| 14 | +import torch | ||
| 15 | + | ||
| 16 | +from .moe_unpacked_module_example import SparseMoeBlockWithLinearExperts # pylint: disable=relative-beyond-top-level | ||
| 17 | + | ||
| 18 | + | ||
| 19 | +def is_packed_moe_tensor(key: str, tensor: torch.Tensor) -> bool: | ||
| 20 | + """Heuristic: packed experts are usually 3D for gate_up/down projections.""" | ||
| 21 | + if not isinstance(tensor, torch.Tensor): | ||
| 22 | + return False | ||
| 23 | + if tensor.dim() != 3: | ||
| 24 | + return False | ||
| 25 | + return key.endswith("experts.gate_up_proj") or key.endswith("experts.down_proj") | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +def unpack_packed_moe_weights(state_dict: Dict[str, torch.Tensor]) -> Dict[str, torch.Tensor]: | ||
| 29 | + """ | ||
| 30 | + Unpack common packed MoE keys into Linear-expert keys. | ||
| 31 | + | ||
| 32 | + Expected packed patterns: | ||
| 33 | + - *.experts.gate_up_proj: [num_experts, 2*intermediate, hidden] | ||
| 34 | + - *.experts.down_proj: [num_experts, hidden, intermediate] | ||
| 35 | + """ | ||
| 36 | + unpacked = dict(state_dict) | ||
| 37 | + | ||
| 38 | + for key, tensor in state_dict.items(): | ||
| 39 | + if not is_packed_moe_tensor(key, tensor): | ||
| 40 | + continue | ||
| 41 | + | ||
| 42 | + if key.endswith("experts.gate_up_proj"): | ||
| 43 | + prefix = key.replace("experts.gate_up_proj", "experts.") | ||
| 44 | + num_experts = tensor.shape[0] | ||
| 45 | + for i in range(num_experts): | ||
| 46 | + gate_w, up_w = tensor[i].chunk(2, dim=0) | ||
| 47 | + unpacked[f"{prefix}{i}.gate_proj.weight"] = gate_w | ||
| 48 | + unpacked[f"{prefix}{i}.up_proj.weight"] = up_w | ||
| 49 | + | ||
| 50 | + elif key.endswith("experts.down_proj"): | ||
| 51 | + prefix = key.replace("experts.down_proj", "experts.") | ||
| 52 | + num_experts = tensor.shape[0] | ||
| 53 | + for i in range(num_experts): | ||
| 54 | + unpacked[f"{prefix}{i}.down_proj.weight"] = tensor[i] | ||
| 55 | + | ||
| 56 | + return unpacked | ||
| 57 | + | ||
| 58 | + | ||
| 59 | +def replace_moe_module_if_needed(layer, cfg, act_fn): | ||
| 60 | + """ | ||
| 61 | + Replace fused MoE module with unpacked Linear-expert module. | ||
| 62 | + Call this during layer construction/loading in model_adapter.py. | ||
| 63 | + """ | ||
| 64 | + if not hasattr(layer, "mlp") or not hasattr(layer.mlp, "experts"): | ||
| 65 | + return layer | ||
| 66 | + | ||
| 67 | + # Example: original layer.mlp uses packed expert layout. | ||
| 68 | + unpacked_moe = SparseMoeBlockWithLinearExperts( | ||
| 69 | + hidden_size=cfg.hidden_size, | ||
| 70 | + intermediate_size=cfg.moe_intermediate_size, | ||
| 71 | + num_experts=cfg.num_experts, | ||
| 72 | + top_k=cfg.num_experts_per_tok, | ||
| 73 | + act_fn=act_fn, | ||
| 74 | + ) | ||
| 75 | + layer.mlp = unpacked_moe | ||
| 76 | + return layer | ||
| 77 | + | ||
| 78 | + | ||
| 79 | +def load_layer_with_unpack_example(layer, layer_state_dict: Dict[str, torch.Tensor], strict: bool = False): | ||
| 80 | + """ | ||
| 81 | + Example load sequence inside adapter: | ||
| 82 | + 1) unpack packed MoE tensors | ||
| 83 | + 2) load state dict into replaced module structure | ||
| 84 | + """ | ||
| 85 | + unpacked_state_dict = unpack_packed_moe_weights(layer_state_dict) | ||
| 86 | + missing, unexpected = layer.load_state_dict(unpacked_state_dict, strict=strict) | ||
| 87 | + return { | ||
| 88 | + "missing_keys": list(missing), | ||
| 89 | + "unexpected_keys": list(unexpected), | ||
| 90 | + "loaded_keys": len(unpacked_state_dict), | ||
| 91 | + } | ||
| @@ -0,0 +1,89 @@ | |||
| 1 | +#!/usr/bin/env python | ||
| 2 | +# -*- coding: UTF-8 -*- | ||
| 3 | +""" | ||
| 4 | +MoE unpacked module example. | ||
| 5 | + | ||
| 6 | +Purpose: | ||
| 7 | +- Provide a pure-nn.Linear MoE implementation for models whose original experts | ||
| 8 | + are fused/packed in 3D tensors. | ||
| 9 | +- Keep routing logic and forward behavior consistent with the source model. | ||
| 10 | + | ||
| 11 | +Note: | ||
| 12 | +- This is an adaptation example, not a drop-in module for every model. | ||
| 13 | +- You must align tensor layouts and routing semantics with the original modeling file. | ||
| 14 | +""" | ||
| 15 | + | ||
| 16 | +from typing import Callable | ||
| 17 | + | ||
| 18 | +import torch | ||
| 19 | +from torch import nn | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +class MoeExpertMLP(nn.Module): | ||
| 23 | + """Single expert with pure Linear layers.""" | ||
| 24 | + | ||
| 25 | + def __init__(self, hidden_size: int, intermediate_size: int, act_fn: Callable[[torch.Tensor], torch.Tensor]): | ||
| 26 | + super().__init__() | ||
| 27 | + self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias=False) | ||
| 28 | + self.up_proj = nn.Linear(hidden_size, intermediate_size, bias=False) | ||
| 29 | + self.down_proj = nn.Linear(intermediate_size, hidden_size, bias=False) | ||
| 30 | + self.act_fn = act_fn | ||
| 31 | + | ||
| 32 | + def forward(self, x: torch.Tensor) -> torch.Tensor: | ||
| 33 | + gate = self.act_fn(self.gate_proj(x)) | ||
| 34 | + up = self.up_proj(x) | ||
| 35 | + return self.down_proj(gate * up) | ||
| 36 | + | ||
| 37 | + | ||
| 38 | +class TopKRouter(nn.Module): | ||
| 39 | + """Generic top-k router example.""" | ||
| 40 | + | ||
| 41 | + def __init__(self, hidden_size: int, num_experts: int, top_k: int): | ||
| 42 | + super().__init__() | ||
| 43 | + self.top_k = top_k | ||
| 44 | + self.weight = nn.Parameter(torch.empty((num_experts, hidden_size))) | ||
| 45 | + | ||
| 46 | + def forward(self, hidden_states: torch.Tensor): | ||
| 47 | + logits = torch.nn.functional.linear(hidden_states, self.weight) # pylint: disable=not-callable | ||
| 48 | + probs = torch.softmax(logits, dim=-1, dtype=torch.float) | ||
| 49 | + topk_prob, topk_idx = torch.topk(probs, self.top_k, dim=-1) | ||
| 50 | + topk_prob = topk_prob / topk_prob.sum(dim=-1, keepdim=True) | ||
| 51 | + return logits, topk_prob.to(logits.dtype), topk_idx | ||
| 52 | + | ||
| 53 | + | ||
| 54 | +class SparseMoeBlockWithLinearExperts(nn.Module): | ||
| 55 | + """ | ||
| 56 | + MoE block where each expert is explicit Linear layers. | ||
| 57 | + This is the target structure after unpack. | ||
| 58 | + """ | ||
| 59 | + | ||
| 60 | + def __init__( | ||
| 61 | + self, | ||
| 62 | + hidden_size: int, | ||
| 63 | + intermediate_size: int, | ||
| 64 | + num_experts: int, | ||
| 65 | + top_k: int, | ||
| 66 | + act_fn: Callable[[torch.Tensor], torch.Tensor], | ||
| 67 | + ): | ||
| 68 | + super().__init__() | ||
| 69 | + self.num_experts = num_experts | ||
| 70 | + self.router = TopKRouter(hidden_size=hidden_size, num_experts=num_experts, top_k=top_k) | ||
| 71 | + self.experts = nn.ModuleList([MoeExpertMLP(hidden_size, intermediate_size, act_fn) for _ in range(num_experts)]) | ||
| 72 | + | ||
| 73 | + def forward(self, hidden_states: torch.Tensor) -> torch.Tensor: | ||
| 74 | + batch_size, seq_len, hidden_size = hidden_states.shape | ||
| 75 | + flat = hidden_states.reshape(-1, hidden_size) | ||
| 76 | + _, routing_weights, selected_experts = self.router(flat) | ||
| 77 | + | ||
| 78 | + out = torch.zeros_like(flat) | ||
| 79 | + # pylint: disable-next=not-callable | ||
| 80 | + expert_mask = torch.nn.functional.one_hot(selected_experts, num_classes=self.num_experts).permute(2, 1, 0) | ||
| 81 | + active_experts = torch.nonzero(expert_mask.sum(dim=(-1, -2)) > 0).flatten() | ||
| 82 | + | ||
| 83 | + for expert_idx in active_experts.tolist(): | ||
| 84 | + top_k_pos, token_idx = torch.where(expert_mask[expert_idx]) | ||
| 85 | + expert_out = self.experts[expert_idx](flat[token_idx]) | ||
| 86 | + expert_out = expert_out * routing_weights[token_idx, top_k_pos, None] | ||
| 87 | + out.index_add_(0, token_idx, expert_out.to(out.dtype)) | ||
| 88 | + | ||
| 89 | + return out.reshape(batch_size, seq_len, hidden_size) | ||
| @@ -0,0 +1,196 @@ | |||
| 1 | +--- | ||
| 2 | +name: dequant | ||
| 3 | +description: 为 msModelSlim 适配流程注入反量化能力。先识别模型权重是否可反量化,再实现反量化脚本并接入 model_adapter。当前仅覆盖 FP8 的 per-block 与 per-channel 两类;若格式不确定或无公开反量化规则,要求用户提供反量化脚本或浮点权重。 | ||
| 4 | +metadata: | ||
| 5 | + # subagent 绑定声明:id = 委派标识(subagent_type);bind = 委派时须绑定加载的 skill 根目录(含 references/、scripts/) | ||
| 6 | + subagent: | ||
| 7 | + id: "adaptation/calibration/dequant" | ||
| 8 | + bind: | ||
| 9 | + | ||
| 10 | + - "adaptation/calibration/dequant" | ||
| 11 | + | ||
| 12 | +--- | ||
| 13 | + | ||
| 14 | +# 反量化接入(dequant) | ||
| 15 | + | ||
| 16 | +本 Skill 用于在模型适配前处理 FP8 量化权重,将可识别权重恢复为 bf16/fp16 再进入后续适配与量化流程。 | ||
| 17 | + | ||
| 18 | +## 适用范围 | ||
| 19 | + | ||
| 20 | +- 支持: | ||
| 21 | + - FP8 per-block(块级 scale) | ||
| 22 | + - FP8 per-channel(通道级 scale) | ||
| 23 | +- 不支持(默认阻塞): | ||
| 24 | + - 无法确认反量化规则的自定义量化格式 | ||
| 25 | + - 缺少必要 scale/shape 元数据的量化权重 | ||
| 26 | + - 仅有推理框架专用格式且无训练侧可逆映射说明 | ||
| 27 | + - 非 FP8(例如 INT4/INT8/FP4)在本 Skill 中默认不处理 | ||
| 28 | + | ||
| 29 | +## 输入 | ||
| 30 | + | ||
| 31 | +- 模型目录(至少含 `config.json`、`model.safetensors.index.json`) | ||
| 32 | +- 目标模型的 `model_adapter.py` | ||
| 33 | + | ||
| 34 | +## 硬门禁:先识别“可接受反量化权重” | ||
| 35 | + | ||
| 36 | +在编写任何脚本前,必须先判定是否属于可接受类型: | ||
| 37 | + | ||
| 38 | +1. 读取 `config.json`,提取可用量化配置字段(如量化类型、group/block 相关参数) | ||
| 39 | +2. 读取 `model.safetensors.index.json` 的 `weight_map` | ||
| 40 | +3. 按键名模式识别: | ||
| 41 | + - **FP8 per-block 可接受**:存在与权重匹配的块级 scale(常见键后缀如 `xxx.weight_scale_inv` / `xxx.scale`),且可推导块大小 `block_size` | ||
| 42 | + - **FP8 per-channel 可接受**:存在与权重某一维匹配的通道级 scale(常见形态如 `[out_features]`、`[in_features]` 或可广播到对应通道维) | ||
| 43 | +4. 用 scale 形状对权重形状做一致性检查: | ||
| 44 | + - per-block:`M` 与 `N` 可按 `block_size` 分块并与 scale 网格对应 | ||
| 45 | + - per-channel:scale 的长度或广播维度与指定 channel axis 一致 | ||
| 46 | +5. 若未命中上述模式,标记为 **未知/不接受格式** 并停止实现 | ||
| 47 | + | ||
| 48 | +> 必须输出判定依据(命中的键模式、样例键名、对应文件)。 | ||
| 49 | + | ||
| 50 | +## 工作流 | ||
| 51 | + | ||
| 52 | +### 1) 识别与分流 | ||
| 53 | + | ||
| 54 | +- `未量化`:直接跳过反量化步骤,进入后续适配 | ||
| 55 | +- `可接受量化(FP8 per-block/per-channel)`:进入脚本实现与适配器接入 | ||
| 56 | +- `未知/不接受量化`:阻塞并向用户提出材料请求(见“阻塞话术”) | ||
| 57 | + | ||
| 58 | +### 2) 实现反量化脚本(`convert_*_to_bf16.py`) | ||
| 59 | + | ||
| 60 | +按 FP8 模式创建脚本,最小要求: | ||
| 61 | + | ||
| 62 | +- 提供 `convert_*` 主转换函数(必须带可见进度条与异常提示) | ||
| 63 | +- 脚本只需覆盖当前模型确认的量化模式(可仅实现 per-block) | ||
| 64 | +- 将结果统一转换到 `torch.bfloat16`(或项目默认浮点 dtype) | ||
| 65 | +- 参数回写必须 dtype-safe(强约束): | ||
| 66 | + - 若原始权重存储 dtype 为 FP8,禁止直接将反量化得到的 BF16/F16 张量回写到该 FP8 存储;否则会发生静默类型回退 | ||
| 67 | + - 必须先将“原参数存储”提升到目标浮点 dtype(如 BF16),再执行赋值/拷贝 | ||
| 68 | + - 允许不同代码风格实现(原地改写或整体替换),但必须保证 dtype 与 device 一致 | ||
| 69 | + - 回写后必须有显式校验(断言或日志)确认最终参数 dtype 已是目标浮点类型,而非 FP8 | ||
| 70 | +- 对 index 与 safetensors 不匹配场景给出 warning 并安全跳过 | ||
| 71 | +- 进度条要求: | ||
| 72 | + - 必须使用 `tqdm`(或等价方案)显示转换进度,禁止仅打印日志让用户等待 | ||
| 73 | + - `total` 必须可追踪(例如按待转换参数个数/层数),并实时更新 `desc`(如当前层名) | ||
| 74 | + - 长耗时阶段(扫描权重、逐层反量化、保存结果)至少覆盖主耗时环节中的一个;优先覆盖逐层反量化主循环 | ||
| 75 | +- 导出件要求: | ||
| 76 | + | ||
| 77 | + -如果反量化脚本可以离线运行,即可以先运行反量化导出浮点权重,则导出件中必须包含原生权重中的所有文件,**特别**是 `chat_template.jinja` 文件,该文件缺失会导致服务化异常。 | ||
| 78 | + | ||
| 79 | +建议函数骨架: | ||
| 80 | + | ||
| 81 | +```python | ||
| 82 | +@lru_cache(maxsize=1) | ||
| 83 | +def get_weight_map(model_path: str) -> Dict[str, str]: | ||
| 84 | + ... | ||
| 85 | + | ||
| 86 | +@torch.no_grad() | ||
| 87 | +def convert_module_xxx_to_bf16(name: str, module: nn.Module, model_path: str, weight_map: Dict[str, str]): | ||
| 88 | + ... | ||
| 89 | +``` | ||
| 90 | + | ||
| 91 | +per-block 示例: | ||
| 92 | + | ||
| 93 | +```python | ||
| 94 | +def decode_fp8_per_block(weight: torch.Tensor, scale: torch.Tensor, block_size: int = 128) -> torch.Tensor: | ||
| 95 | + m, n = weight.shape | ||
| 96 | + scale_expanded = scale.repeat_interleave(block_size, dim=0).repeat_interleave(block_size, dim=1)[:m, :n] | ||
| 97 | + return (weight.float() * scale_expanded.float()).to(torch.bfloat16) | ||
| 98 | +``` | ||
| 99 | + | ||
| 100 | +参数回写(通用伪代码): | ||
| 101 | + | ||
| 102 | +```python | ||
| 103 | +dequant_weight = dequantize(fp8_weight, scale_info) # dequant_weight: BF16/F16 | ||
| 104 | +if original_param_storage_is_fp8: | ||
| 105 | + promote_original_param_storage_to_target_float_dtype() | ||
| 106 | +assign_or_copy_with_device_dtype_alignment(dequant_weight) | ||
| 107 | +verify_param_dtype_is_target_float_dtype_not_fp8() | ||
| 108 | +``` | ||
| 109 | + | ||
| 110 | +可选工具脚本(用于判定权重/scale 形状): | ||
| 111 | + | ||
| 112 | +- `scripts/inspect_safetensors.py` | ||
| 113 | +- 用法: | ||
| 114 | + - `python scripts/inspect_safetensors.py -m /path/to/model` | ||
| 115 | + - `python scripts/inspect_safetensors.py -m /path/to/model -p "model.layers.*.mlp*.weight*"` | ||
| 116 | + | ||
| 117 | +### 3) 接入 `model_adapter.py` | ||
| 118 | + | ||
| 119 | +在适配器中注入反量化调用,要求: | ||
| 120 | + | ||
| 121 | +- 在调用 transformers 加载模型前,先检查模型目录 `config.json` 是否包含量化字段(如 `quantization_config`、`quant_method`、`fmt`、`weight_block_size`、`modules_to_not_convert` 等) | ||
| 122 | +- 若检测到量化字段,必须主动删除这些字段后再进行加载,避免 transformers 按量化方式装载权重 | ||
| 123 | +- 删除原因必须在日志中说明:昇腾当前不支持 FP8 量化权重直载,若不清理配置会导致错误加载路径 | ||
| 124 | +- 可接受实现方式: | ||
| 125 | + - 直接修改模型目录下 `config.json` 后加载;或 | ||
| 126 | + - 生成去量化字段的临时 `config` 并确保后续加载明确使用该配置 | ||
| 127 | +- 无论采用哪种方式,都必须保证“最终传给 transformers 的 config 不包含量化相关字段” | ||
| 128 | +- 在模型权重可用后、量化流程前调用 `convert_*_to_bf16` | ||
| 129 | +- 仅对命中的量化子模块执行,避免全模型无谓遍历 | ||
| 130 | +- 转换失败不应静默吞掉;至少记录 warning 与原因 | ||
| 131 | + | ||
| 132 | +接入示例: | ||
| 133 | + | ||
| 134 | +```python | ||
| 135 | +from .convert_fp8_to_bf16 import convert_module_fp8_to_bf16 | ||
| 136 | + | ||
| 137 | +def init_model(...): | ||
| 138 | + model = ... | ||
| 139 | + convert_module_fp8_to_bf16(name="", module=model, model_path=model_path, weight_map=...) | ||
| 140 | + return model | ||
| 141 | +``` | ||
| 142 | + | ||
| 143 | +### 4) 最小验证 | ||
| 144 | + | ||
| 145 | +- 校验可读性:脚本能读取 index 与目标 safetensors | ||
| 146 | +- 校验转换:目标层权重 dtype 由 FP8 变为 bf16/fp16 | ||
| 147 | +- 校验接入:adapter 初始化路径已触发自动转换函数 | ||
| 148 | +- 校验回退:非量化权重或不匹配键时不报致命错误 | ||
| 149 | +- 校验模式:输出明确 `per-block` 或 `per-channel` 判定结果 | ||
| 150 | +- 校验赋值安全:确认未出现“BF16 copy_ 到 FP8 存储后仍为 FP8”的回退问题 | ||
| 151 | + | ||
| 152 | +## 阻塞话术(必须原样遵循语义) | ||
| 153 | + | ||
| 154 | +当量化格式不确定或缺反量化规则时,必须明确告知: | ||
| 155 | + | ||
| 156 | +- 当前权重格式无法确认可逆反量化规则,不能安全实现通用反量化。 | ||
| 157 | +- 请用户二选一提供: | ||
| 158 | + 1. 官方/已验证的反量化脚本(含权重键映射与scale定义) | ||
| 159 | + 2. 对应浮点权重(bf16/fp16/fp32) | ||
| 160 | +- 在补齐材料前,不进入模型适配代码改写阶段。 | ||
| 161 | + | ||
| 162 | +## 输出模板 | ||
| 163 | + | ||
| 164 | +Agent 每次执行本 Skill 后,输出以下结构: | ||
| 165 | + | ||
| 166 | +```markdown | ||
| 167 | + | ||
| 168 | +## 反量化判定结果 | ||
| 169 | +- 量化状态:未量化 | FP8 per-block(可接受) | FP8 per-channel(可接受) | 未知(阻塞) | ||
| 170 | +- 判定依据:{keys/file evidence} | ||
| 171 | + | ||
| 172 | +## 反量化实现动作 | ||
| 173 | +- 新增/复用脚本:{path} | ||
| 174 | +- 核心函数:{functions} | ||
| 175 | +- 适配器接入点:{adapter_path + function} | ||
| 176 | + | ||
| 177 | +## 验证结果 | ||
| 178 | +- 读取校验:通过/失败 | ||
| 179 | +- dtype 校验:通过/失败 | ||
| 180 | +- 适配器触发校验:通过/失败 | ||
| 181 | + | ||
| 182 | +## 后续动作 | ||
| 183 | +- 若阻塞:请求用户提供反量化脚本或浮点权重 | ||
| 184 | +- 若通过:进入模型适配主流程 | ||
| 185 | + | ||
| 186 | +## 经验条目(Experiences,追加制) | ||
| 187 | + | ||
| 188 | +> 追加规范见 `skills/README.md`「经验条目」。连续编号 `[E-序号]`;正文保留权威展开,本表只做索引 + 元数据登记;来源:实测编号(F0-x / Dx / Tx)| 用户反馈 | 代码实证;验证状态:已回归 | 待验证 | 已上流 docs。 | ||
| 189 | + | ||
| 190 | +| 条目 | 主题 | 适用条件 / 触发信号 | 结论要点(一句话) | 正文位置 | 来源 | 验证状态 | | ||
| 191 | +|------|------|------|------|------|------|------| | ||
| 192 | +| E-001 | dtype-safe 参数回写 | FP8 权重反量化回写 | 原参数存储为 FP8 时,必须先提升存储到目标浮点 dtype 再赋值,否则 BF16/F16 `copy_` 到 FP8 存储发生静默类型回退;回写后显式校验最终 dtype | 2) 实现反量化脚本-参数回写 | 代码实证 | 已回归 | | ||
| 193 | +| E-002 | 导出件完整性 | 反量化可离线运行时 | 导出件必须包含原生权重全部文件,**尤其 `chat_template.jinja`**——缺失会导致服务化异常 | 2) 实现反量化脚本-导出件要求 | 用户反馈 | 待验证 | | ||
| 194 | +| E-003 | 加载前清理量化字段 | adapter 接入反量化 | 加载前检查并删除 `config.json` 量化字段(`quantization_config`/`quant_method`/`fmt`/`weight_block_size` 等),昇腾不支持 FP8 量化权重直载;删除原因须日志说明 | 3) 接入 model_adapter | 代码实证 | 已回归 | | ||
| 195 | +| E-004 | 格式不确定即阻塞 | 无法确认可逆反量化规则 | 不得安全实现通用反量化:按「阻塞话术」要求用户二选一提供官方反量化脚本或浮点权重,补齐前不进入适配改写 | 阻塞话术 | 用户反馈 | 已回归 | | ||
| 196 | +``` | ||
| @@ -0,0 +1,81 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +# -*- coding: UTF-8 -*- | ||
| 3 | + | ||
| 4 | +import argparse | ||
| 5 | +import fnmatch | ||
| 6 | +import json | ||
| 7 | +import os | ||
| 8 | +from collections import defaultdict | ||
| 9 | + | ||
| 10 | +from safetensors import safe_open | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +def parse_args(): | ||
| 14 | + parser = argparse.ArgumentParser(description="Inspect tensor shape and dtype from safetensors by wildcard pattern.") | ||
| 15 | + parser.add_argument( | ||
| 16 | + "-m", | ||
| 17 | + "--model-path", | ||
| 18 | + required=True, | ||
| 19 | + help="Model directory containing model.safetensors.index.json", | ||
| 20 | + ) | ||
| 21 | + parser.add_argument( | ||
| 22 | + "-p", | ||
| 23 | + "--pattern", | ||
| 24 | + default="*", | ||
| 25 | + help="Wildcard pattern for tensor names, e.g. 'model.layers.*.mlp*.weight*'", | ||
| 26 | + ) | ||
| 27 | + parser.add_argument( | ||
| 28 | + "--limit", | ||
| 29 | + type=int, | ||
| 30 | + default=0, | ||
| 31 | + help="Limit output rows, 0 means no limit", | ||
| 32 | + ) | ||
| 33 | + return parser.parse_args() | ||
| 34 | + | ||
| 35 | + | ||
| 36 | +def load_weight_map(model_path: str): | ||
| 37 | + index_path = os.path.join(model_path, "model.safetensors.index.json") | ||
| 38 | + with open(index_path, "r", encoding="utf-8") as f: | ||
| 39 | + model_index = json.load(f) | ||
| 40 | + return model_index.get("weight_map", {}) | ||
| 41 | + | ||
| 42 | + | ||
| 43 | +def main(): | ||
| 44 | + args = parse_args() | ||
| 45 | + weight_map = load_weight_map(args.model_path) | ||
| 46 | + if not weight_map: | ||
| 47 | + print("No weight_map found in model.safetensors.index.json") | ||
| 48 | + return | ||
| 49 | + | ||
| 50 | + filtered_names = [name for name in weight_map if fnmatch.fnmatch(name, args.pattern)] | ||
| 51 | + if not filtered_names: | ||
| 52 | + print(f"No tensors matched pattern: {args.pattern}") | ||
| 53 | + return | ||
| 54 | + | ||
| 55 | + file_to_names = defaultdict(list) | ||
| 56 | + for name in filtered_names: | ||
| 57 | + file_to_names[weight_map[name]].append(name) | ||
| 58 | + | ||
| 59 | + rows = [] | ||
| 60 | + for file_name, names in sorted(file_to_names.items()): | ||
| 61 | + file_path = os.path.join(args.model_path, file_name) | ||
| 62 | + with safe_open(file_path, framework="pt", device="cpu") as f: | ||
| 63 | + available = set(f.keys()) | ||
| 64 | + for name in sorted(names): | ||
| 65 | + if name not in available: | ||
| 66 | + rows.append((name, "N/A", "N/A", file_name)) | ||
| 67 | + continue | ||
| 68 | + tensor = f.get_tensor(name) | ||
| 69 | + rows.append((name, str(tuple(tensor.shape)), str(tensor.dtype), file_name)) | ||
| 70 | + | ||
| 71 | + if args.limit > 0: | ||
| 72 | + rows = rows[: args.limit] | ||
| 73 | + | ||
| 74 | + print(f"Matched tensors: {len(rows)} (pattern={args.pattern})") | ||
| 75 | + print("tensor_name\tshape\tdtype\tfile") | ||
| 76 | + for name, shape, dtype, file_name in rows: | ||
| 77 | + print(f"{name}\t{shape}\t{dtype}\t{file_name}") | ||
| 78 | + | ||
| 79 | + | ||
| 80 | +if __name__ == "__main__": | ||
| 81 | + main() | ||
| @@ -0,0 +1,64 @@ | |||
| 1 | +--- | ||
| 2 | +name: layer-wise | ||
| 3 | +description: 为 msModelSlim 适配器实现逐层量化(按层加载/懒加载)能力。仅在用户明确要求逐层量化或基础适配因 CPU 内存不足无法全量加载权重时使用。该特性为高阶可选项,不是基础适配必需项。 | ||
| 4 | +--- | ||
| 5 | + | ||
| 6 | +# 逐层量化(layer-wise) | ||
| 7 | + | ||
| 8 | +用于在基础适配完成后,为超大模型增加“逐层量化(按层加载)”能力,以降低 CPU 内存峰值。 | ||
| 9 | + | ||
| 10 | +## 触发条件 | ||
| 11 | + | ||
| 12 | +- 用户明确要求实现逐层量化/逐层加载/懒加载/按层加载。 | ||
| 13 | +- 或基础适配已完成,但 CPU 内存无法全量加载权重。 | ||
| 14 | + | ||
| 15 | +## 必须前置条件 | ||
| 16 | + | ||
| 17 | +- 已完成基础适配器开发(5个基础接口可用)。 | ||
| 18 | +- 已完成四步验证流程(生成测试模型 -> 全回退量化 -> 权重一致性 -> 实际量化验证)。 | ||
| 19 | +- 模型目录存在 `model.safetensors.index.json`,且可按层定位权重。 | ||
| 20 | + | ||
| 21 | +## 约束声明(必须告知用户) | ||
| 22 | + | ||
| 23 | +- 逐层量化是高阶可选特性,不是基础适配必需项。 | ||
| 24 | +- 该方案用于“内存受限时继续开发”,不保证一定适配成功。 | ||
| 25 | +- 常见失败点:层构造参数不一致、权重键名不匹配、自定义模块副作用、MTP/MoE 特殊路径未覆盖。 | ||
| 26 | + | ||
| 27 | +## 最小实现步骤 | ||
| 28 | + | ||
| 29 | +1. 增加权重索引与按需读取能力: | ||
| 30 | + - `get_weight_map`(缓存 `model.safetensors.index.json`) | ||
| 31 | + - `get_state_dict(module, prefix)`(只读取当前层所需权重) | ||
| 32 | +2. 实现按层实例化与加载: | ||
| 33 | + - `load_decoder_if_not_exist(model, name, idx)` | ||
| 34 | + - 缺层时按模板层构造并加载该层 state_dict。 | ||
| 35 | +3. 实现逐层遍历器: | ||
| 36 | + - `generate_decoder_layer(model)` 按 `num_hidden_layers` 逐层 `yield`。 | ||
| 37 | +4. 在 `generate_model_visit` 与 `generate_model_forward` 统一使用逐层遍历器,保持严格同序。 | ||
| 38 | + | ||
| 39 | +## 最小验证 | ||
| 40 | + | ||
| 41 | +- `generate_decoder_layer` 能完整遍历全部层。 | ||
| 42 | +- `generate_model_visit` 与 `generate_model_forward` 无层序错位。 | ||
| 43 | +- 抽样 1-2层前向结果 shape 连续、无异常。 | ||
| 44 | + | ||
| 45 | +## 参考资料 | ||
| 46 | + | ||
| 47 | +- [逐层量化工作流](references/workflow.md) | ||
| 48 | +- [逐层量化实现指南](references/implementation_guide.md) | ||
| 49 | +- [逐层适配示例代码](references/layerwise_adapter_example.py) | ||
| 50 | + | ||
| 51 | +## 结束输出要求 | ||
| 52 | + | ||
| 53 | +- 明确告知用户是否满足触发条件。 | ||
| 54 | +- 若满足,给出接入点、最小改造范围与验证结果。 | ||
| 55 | +- 若不满足,建议先完成基础适配与四步验证,不提前进入逐层量化。 | ||
| 56 | + | ||
| 57 | +## 经验条目(Experiences,追加制) | ||
| 58 | + | ||
| 59 | +> 追加规范见 `skills/README.md`「经验条目」。连续编号 `[E-序号]`;正文保留权威展开,本表只做索引 + 元数据登记;来源:实测编号(F0-x / Dx / Tx)| 用户反馈 | 代码实证;验证状态:已回归 | 待验证 | 已上流 docs。 | ||
| 60 | + | ||
| 61 | +| 条目 | 主题 | 适用条件 / 触发信号 | 结论要点(一句话) | 正文位置 | 来源 | 验证状态 | | ||
| 62 | +|------|------|------|------|------|------|------| | ||
| 63 | +| E-001 | 逐层量化常见失败点 | 实现逐层加载时 | 层构造参数不一致、权重键名不匹配、自定义模块副作用、MTP/MoE 特殊路径未覆盖是最常见失败点;该方案不保证一定适配成功 | 约束声明 | 代码实证 | 待验证 | | ||
| 64 | +| E-002 | visit/forward 同序 | 改造逐层遍历器 | `generate_model_visit` 与 `generate_model_forward` 必须统一使用同一逐层遍历器,保持严格同序,防止层序错位 | 最小实现步骤 4 | 代码实证 | 已回归 | | ||
| @@ -0,0 +1,36 @@ | |||
| 1 | +# 逐层量化实现指南 | ||
| 2 | + | ||
| 3 | +## 关键实现点 | ||
| 4 | + | ||
| 5 | +### 1) 权重索引缓存 | ||
| 6 | + | ||
| 7 | +- 从 `model.safetensors.index.json` 读取 `weight_map`。 | ||
| 8 | +- 对 `weight_map` 做缓存,避免重复解析。 | ||
| 9 | + | ||
| 10 | +### 2) 单层按需加载 | ||
| 11 | + | ||
| 12 | +- 实现 `get_state_dict(module, prefix)`。 | ||
| 13 | +- 按 `prefix` 过滤当前层参数,只读取当前层所需 safetensors 切片。 | ||
| 14 | + | ||
| 15 | +### 3) 缺层按需构造 | ||
| 16 | + | ||
| 17 | +- 实现 `load_decoder_if_not_exist(model, name, idx)`。 | ||
| 18 | +- 当目标层不存在时,用模板层构造 `idx` 层并加载该层 state_dict。 | ||
| 19 | +- 不要硬编码层构造参数,按目标模型真实 block 构造签名适配。 | ||
| 20 | + | ||
| 21 | +### 4) 统一层遍历器 | ||
| 22 | + | ||
| 23 | +- 实现 `generate_decoder_layer(model)`。 | ||
| 24 | +- 在 `generate_model_visit` 与 `generate_model_forward` 统一调用该遍历器,保持严格同序。 | ||
| 25 | + | ||
| 26 | +## 典型风险点 | ||
| 27 | + | ||
| 28 | +- 层构造参数不兼容(例如无 `layer_idx`)。 | ||
| 29 | +- state_dict 键名前缀与模型层路径不一致。 | ||
| 30 | +- 远程代码中的自定义模块在懒加载时有副作用。 | ||
| 31 | +- MTP/MoE 特殊路径未接入逐层逻辑。 | ||
| 32 | + | ||
| 33 | +## 完成判定 | ||
| 34 | + | ||
| 35 | +- 全层可遍历、visit/forward 同序、抽样前向正常。 | ||
| 36 | +- 不因逐层改造破坏基础四步验证能力。 | ||
| @@ -0,0 +1,53 @@ | |||
| 1 | +import os.path | ||
| 2 | +from collections import defaultdict | ||
| 3 | +from functools import lru_cache | ||
| 4 | +from unittest.mock import patch | ||
| 5 | + | ||
| 6 | +from safetensors import safe_open | ||
| 7 | +from torch import nn | ||
| 8 | + | ||
| 9 | +from msmodelslim.utils.security import json_safe_load | ||
| 10 | + | ||
| 11 | + | ||
| 12 | +class LayerwiseMixin: | ||
| 13 | + | ||
| 14 | + def get_weight_map(self): | ||
| 15 | + index_path = os.path.join(self.model_path, "model.safetensors.index.json") | ||
| 16 | + model_index = json_safe_load(index_path) | ||
| 17 | + return model_index["weight_map"] | ||
| 18 | + | ||
| 19 | + def get_state_dict(self, module: nn.Module, prefix: str = ""): | ||
| 20 | + weight_map = self.get_weight_map() | ||
| 21 | + file_to_names = defaultdict(list) | ||
| 22 | + for name, _ in module.named_parameters(): | ||
| 23 | + full_name = f"{prefix}.{name}" if prefix else name | ||
| 24 | + if full_name in weight_map: | ||
| 25 | + file_to_names[weight_map[full_name]].append(name) | ||
| 26 | + | ||
| 27 | + state_dict = {} | ||
| 28 | + for file_name, names in file_to_names.items(): | ||
| 29 | + file_path = os.path.join(self.model_path, file_name) | ||
| 30 | + with safe_open(file_path, framework="pt", device="cpu") as f: | ||
| 31 | + for name in names: | ||
| 32 | + full_name = f"{prefix}.{name}" if prefix else name | ||
| 33 | + state_dict[name] = f.get_tensor(full_name) | ||
| 34 | + return state_dict | ||
| 35 | + | ||
| 36 | + def load_decoder_if_not_exist(self, model: nn.Module, name: str, idx: int): | ||
| 37 | + try: | ||
| 38 | + return model.get_submodule(name) | ||
| 39 | + except AttributeError: | ||
| 40 | + with patch.object(nn.Linear, "reset_parameters", lambda _self: None): | ||
| 41 | + module_list: nn.ModuleList = model.model.layers | ||
| 42 | + template_module = module_list[0] | ||
| 43 | + decoder = template_module.__class__(config=self.config, layer_idx=idx) | ||
| 44 | + state_dict = self.get_state_dict(decoder, prefix=name) | ||
| 45 | + decoder.load_state_dict(state_dict) | ||
| 46 | + decoder.eval() | ||
| 47 | + module_list.append(decoder) | ||
| 48 | + return decoder | ||
| 49 | + | ||
| 50 | + def generate_decoder_layer(self, model: nn.Module): | ||
| 51 | + for idx in range(self.config.num_hidden_layers): | ||
| 52 | + name = f"model.layers.{idx}" | ||
| 53 | + yield name, self.load_decoder_if_not_exist(model, name=name, idx=idx) | ||
| @@ -0,0 +1,27 @@ | |||
| 1 | +# 逐层量化工作流 | ||
| 2 | + | ||
| 3 | +用于在基础适配与四步验证通过后,开启按层加载量化路径。 | ||
| 4 | + | ||
| 5 | +## 阶段 A:确认是否需要启用 | ||
| 6 | + | ||
| 7 | +- 用户明确要求逐层量化/逐层加载/懒加载。 | ||
| 8 | +- 或 CPU 内存无法全量加载权重。 | ||
| 9 | +- 若不满足,保持基础适配流程,不进入本工作流。 | ||
| 10 | + | ||
| 11 | +## 阶段 B:逐层改造 | ||
| 12 | + | ||
| 13 | +1. 增加权重映射读取能力(`model.safetensors.index.json`)。 | ||
| 14 | +2. 增加按需读取单层权重能力(按 prefix 过滤)。 | ||
| 15 | +3. 增加缺层时的按层实例化与加载逻辑。 | ||
| 16 | +4. 统一 `generate_model_visit` 与 `generate_model_forward` 的层遍历来源,确保严格同序。 | ||
| 17 | + | ||
| 18 | +## 阶段 C:最小验证 | ||
| 19 | + | ||
| 20 | +1. 可完整遍历所有 decoder 层。 | ||
| 21 | +2. visit/forward 层序一一对应,无错位。 | ||
| 22 | +3. 抽样前向无 shape 断裂与异常。 | ||
| 23 | + | ||
| 24 | +## 验收规则 | ||
| 25 | + | ||
| 26 | +- 若阶段 B/C 全通过,可标记“逐层量化改造完成”。 | ||
| 27 | +- 若失败,回退到基础适配路径并记录阻塞点(层构造参数、键名映射、模块副作用等)。 | ||
| @@ -0,0 +1,20 @@ | |||
| 1 | +apiversion: modelslim_v1 | ||
| 2 | +spec: | ||
| 3 | + process: | ||
| 4 | + - type: "linear_quant" | ||
| 5 | + qconfig: | ||
| 6 | + act: | ||
| 7 | + scope: "per_token" | ||
| 8 | + dtype: "int8" | ||
| 9 | + symmetric: True | ||
| 10 | + method: "minmax" | ||
| 11 | + weight: | ||
| 12 | + scope: "per_channel" | ||
| 13 | + dtype: "int8" | ||
| 14 | + symmetric: True | ||
| 15 | + method: "minmax" | ||
| 16 | + include: ["*"] | ||
| 17 | + exclude: ["*"] # 全部回退,不实际量化任何层 | ||
| 18 | + save: | ||
| 19 | + - type: "ascendv1_saver" | ||
| 20 | + part_file_size: 4 | ||
| @@ -0,0 +1,46 @@ | |||
| 1 | +#!/usr/bin/env python | ||
| 2 | +# -*- coding: UTF-8 -*- | ||
| 3 | + | ||
| 4 | +""" | ||
| 5 | +基础模型适配器模板 | ||
| 6 | +适用于 HuggingFace Transformers 的 Decoder-only LLM。 | ||
| 7 | +""" | ||
| 8 | + | ||
| 9 | +from typing import List, Any, Generator | ||
| 10 | + | ||
| 11 | +from torch import nn | ||
| 12 | +from msmodelslim.core.base.protocol import ProcessRequest | ||
| 13 | +from msmodelslim.core.const import DeviceType | ||
| 14 | +from msmodelslim.model.common.layer_wise_forward import ( | ||
| 15 | + generated_decoder_layer_visit_func, | ||
| 16 | + transformers_generated_forward_func, | ||
| 17 | +) | ||
| 18 | +from msmodelslim.model.common.transformers import TransformersModel | ||
| 19 | +from msmodelslim.model.interface_hub import ModelInfoInterface, ModelSlimPipelineInterfaceV1 | ||
| 20 | +from msmodelslim.utils.logging import logger_setter | ||
| 21 | + | ||
| 22 | + | ||
| 23 | + | ||
| 24 | +class MyModelAdapter(TransformersModel, ModelInfoInterface, ModelSlimPipelineInterfaceV1): # pylint: disable=too-many-ancestors | ||
| 25 | + # ==================== ModelInfoInterface ==================== | ||
| 26 | + def get_model_pedigree(self) -> str: | ||
| 27 | + return "my_model" | ||
| 28 | + | ||
| 29 | + def get_model_type(self) -> str: | ||
| 30 | + return self.model_type | ||
| 31 | + | ||
| 32 | + # ==================== ModelSlimPipelineInterfaceV1 ==================== | ||
| 33 | + def handle_dataset(self, dataset: Any, device: DeviceType = DeviceType.NPU) -> List[Any]: | ||
| 34 | + return self._get_tokenized_data(dataset, device) | ||
| 35 | + | ||
| 36 | + def init_model(self, device: DeviceType = DeviceType.NPU) -> nn.Module: | ||
| 37 | + return self._load_model(device) | ||
| 38 | + | ||
| 39 | + def generate_model_visit(self, model: nn.Module) -> Generator[ProcessRequest, Any, None]: | ||
| 40 | + yield from generated_decoder_layer_visit_func(model) | ||
| 41 | + | ||
| 42 | + def generate_model_forward(self, model: nn.Module, inputs: Any) -> Generator[ProcessRequest, Any, None]: | ||
| 43 | + yield from transformers_generated_forward_func(model, inputs) | ||
| 44 | + | ||
| 45 | + def enable_kv_cache(self, model: nn.Module, need_kv_cache: bool) -> None: | ||
| 46 | + return self._enable_kv_cache(model, need_kv_cache) | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | +apiversion: modelslim_v1 | ||
| 2 | + | ||
| 3 | +spec: | ||
| 4 | + process: | ||
| 5 | + - type: "linear_quant" | ||
| 6 | + qconfig: | ||
| 7 | + act: | ||
| 8 | + scope: "per_token" # dynamic | ||
| 9 | + dtype: "int8" | ||
| 10 | + symmetric: True | ||
| 11 | + method: "minmax" | ||
| 12 | + weight: | ||
| 13 | + scope: "per_channel" | ||
| 14 | + dtype: "int8" | ||
| 15 | + symmetric: True | ||
| 16 | + method: "minmax" | ||
| 17 | + include: ["*"] # 全模型量化 | ||
| 18 | + exclude: [] # 不回退 | ||
| 19 | + | ||
| 20 | + save: | ||
| 21 | + - type: "ascendv1_saver" | ||
| 22 | + part_file_size: 4 | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | +apiversion: modelslim_v1 | ||
| 2 | + | ||
| 3 | +spec: | ||
| 4 | + process: | ||
| 5 | + - type: "linear_quant" | ||
| 6 | + qconfig: | ||
| 7 | + act: | ||
| 8 | + scope: "per_tensor" # static | ||
| 9 | + dtype: "int8" | ||
| 10 | + symmetric: False | ||
| 11 | + method: "minmax" | ||
| 12 | + weight: | ||
| 13 | + scope: "per_channel" | ||
| 14 | + dtype: "int8" | ||
| 15 | + symmetric: True | ||
| 16 | + method: "minmax" | ||
| 17 | + include: ["*"] # 全模型量化 | ||
| 18 | + exclude: [] # 不回退 | ||
| 19 | + | ||
| 20 | + save: | ||
| 21 | + - type: "ascendv1_saver" | ||
| 22 | + part_file_size: 4 | ||
| @@ -0,0 +1,28 @@ | |||
| 1 | +# 核心工作流(创建 + 独立验证触发) | ||
| 2 | + | ||
| 3 | +本 Skill 负责基础适配器创建,并在完成后触发独立验证 Skill。 | ||
| 4 | + | ||
| 5 | +## 阶段 0:权重来源确认(先确认再下载) | ||
| 6 | + | ||
| 7 | +1. 先要求用户提供模型权重(本地目录或可访问路径)。 | ||
| 8 | +2. 若用户已提供权重,直接使用用户权重继续后续流程。 | ||
| 9 | +3. 仅当用户明确确认“没有可用权重”时,再协助下载模型。 | ||
| 10 | +4. 结构分析可先下载非权重文件;进入完整量化与验证前,需确保权重已补齐。 | ||
| 11 | + | ||
| 12 | +## 阶段 A:创建适配器 | ||
| 13 | + | ||
| 14 | +1. 选择模板: | ||
| 15 | + - LLM 使用 `model_adapter_template.py` | ||
| 16 | + - VLM 文本路径使用 `vlm_model_adapter_template.py` | ||
| 17 | +2. 实现必需接口。 | ||
| 18 | +3. 在 `config/config.ini` 中注册模型类型与入口。 | ||
| 19 | + | ||
| 20 | +## 阶段 B:触发功能性验证(独立 Skill) | ||
| 21 | + | ||
| 22 | +1. 适配器开发完成后,明确告知用户可自动执行功能性验证。 | ||
| 23 | +2. 调用独立 Skill:`adaptation/calibration/verify`。 | ||
| 24 | +3. 由该 Skill 按四步自动完成验证并返回结果。 | ||
| 25 | + | ||
| 26 | +## 验收规则 | ||
| 27 | + | ||
| 28 | +仅当阶段 A 完成,且阶段 B 的独立验证 Skill 返回通过时,才将适配器标记为完成。 | ||
| @@ -0,0 +1,143 @@ | |||
| 1 | +# 适配器实现指南 | ||
| 2 | + | ||
| 3 | +## 目录结构(建议) | ||
| 4 | + | ||
| 5 | +一个模型适配器目录通常至少包含以下文件: | ||
| 6 | + | ||
| 7 | +```text | ||
| 8 | +msmodelslim/model/<model_type>/ | ||
| 9 | +├── __init__.py | ||
| 10 | +├── model_adapter.py | ||
| 11 | +└── model.py | ||
| 12 | +``` | ||
| 13 | + | ||
| 14 | +- `__init__.py`:必须存在,保证目录可作为 Python 包被导入 | ||
| 15 | +- `model_adapter.py`:适配器入口与 5个必需接口实现 | ||
| 16 | +- `model.py`:模型结构相关实现(如分层访问/前向辅助逻辑) | ||
| 17 | + | ||
| 18 | +如果该模型已有其他依赖文件(如 `utils.py`、`configuration_*.py`),按实际需要补充,但不要省略 `__init__.py`。 | ||
| 19 | + | ||
| 20 | +## 必需接口 | ||
| 21 | + | ||
| 22 | +1. `handle_dataset` | ||
| 23 | +2. `init_model` | ||
| 24 | +3. `generate_model_visit` | ||
| 25 | +4. `generate_model_forward` | ||
| 26 | +5. `enable_kv_cache` | ||
| 27 | + | ||
| 28 | +## 按模板区分:LLM / VLM 必要接口 | ||
| 29 | + | ||
| 30 | +以下结论基于 `assets/model_adapter_template.py` 与 `assets/vlm_model_adapter_template.py`。 | ||
| 31 | + | ||
| 32 | +### LLM(Decoder-only) | ||
| 33 | + | ||
| 34 | +- **推荐继承**:`TransformersModel + ModelSlimPipelineInterfaceV1`(`ModelInfoInterface` 可选但建议) | ||
| 35 | +- **必须实现**(5个):`handle_dataset`、`init_model`、`generate_model_visit`、`generate_model_forward`、`enable_kv_cache` | ||
| 36 | +- **模板中常见辅助方法**(非框架强制):`_create_model_instance` 等模型初始化辅助函数 | ||
| 37 | + | ||
| 38 | +### VLM(多模态理解,仅图文理解) | ||
| 39 | + | ||
| 40 | +- **推荐继承**:`VLMBaseModelAdapter + ModelSlimPipelineInterfaceV1`(`ModelInfoInterface` 可选但建议) | ||
| 41 | +- **必须实现**(5个):`handle_dataset`、`init_model`、`generate_model_visit`、`generate_model_forward`、`enable_kv_cache` | ||
| 42 | +- **模板中常见辅助方法**(非框架强制):`_create_model_instance` 等模型初始化辅助函数 | ||
| 43 | + | ||
| 44 | +## 特殊情况(需要单独处理) | ||
| 45 | + | ||
| 46 | +### LLM 特殊情况 | ||
| 47 | + | ||
| 48 | +- **decoder 路径不一致**:不一定是 `model.layers`,也可能是 `model.decoder.layers` 或其他路径;必须改 `_decoder_layer_prefix`。 | ||
| 49 | +- **MoE packed 权重**:若为 3D packed experts,需先 unpack,再替换为线性层专家模块。 | ||
| 50 | +- **非标准配置字段**:若无 `num_hidden_layers` 或字段名不同,`init_model` 要按目标 config 改写。 | ||
| 51 | +- **tokenizer 无 pad_token**:若 `tokenizer.pad_token` / `pad_token_id` 为 `None`,Step2 全回退量化会在 `padding=True` 时报错。需在适配器中重写 `_load_tokenizer`,将 `pad_token` 回退为 `eos_token`(或模型官方推荐 pad token)。 | ||
| 52 | + | ||
| 53 | +### VLM 特殊情况 | ||
| 54 | + | ||
| 55 | +- **数据必须图文成对**:`handle_dataset` 需要同时有 `text` 和 `image`,纯文本样本不适配该模板。 | ||
| 56 | +- **视觉/文本路径差异**:模板假设 `model.visual` 与 `model.language_model.layers`,目标模型可能不同,需按真实 `modeling` 改。 | ||
| 57 | +- **融合逻辑不可套模板**:`generate_model_forward` 中 image embeds 注入规则(token id、位置编码、mask)模型差异大,必须对齐官方 forward。 | ||
| 58 | +- **text_config 结构差异**:若不存在 `config.text_config`,需改为模型实际文本配置路径并同步层数字段。 | ||
| 59 | +- **processor 行为差异**:不同模型 `AutoProcessor` 的输入键和 `apply_chat_template` 返回字段不同,需按真实返回值调整 keys。 | ||
| 60 | + | ||
| 61 | +### 接口功能说明(必须落实到代码) | ||
| 62 | + | ||
| 63 | +#### 1) `handle_dataset(dataset, device) -> List[Any]` | ||
| 64 | + | ||
| 65 | +- **职责**:把原始校准样本转成模型可直接消费的输入列表。 | ||
| 66 | +- **输入**:原始数据集(通常是文本 list)和目标设备。 | ||
| 67 | +- **输出**:`List[Any]`,每个元素可直接用于一次前向(如 `model(*data)` 或 `model(**data)`)。 | ||
| 68 | +- **实现建议**:优先复用基类 tokenization 能力(如 `_get_tokenized_data`),保证字段名与模型 forward 参数对齐。 | ||
| 69 | +- **完成判定**:量化流程读取该列表后,无需额外数据转换即可进入 `generate_model_forward`。 | ||
| 70 | + | ||
| 71 | +#### 2) `init_model(device) -> nn.Module` | ||
| 72 | + | ||
| 73 | +- **职责**:初始化并返回可参与量化流程的模型实例。 | ||
| 74 | +- **输入**:目标设备(NPU/CPU)。 | ||
| 75 | +- **输出**:`nn.Module`(`eval()` 状态)。 | ||
| 76 | +- **实现建议**:按模型真实结构加载,确保后续 visit/forward 可访问到目标层。 | ||
| 77 | +- **完成判定**:返回模型后,`generate_model_visit` 能遍历目标量化层,且前向可执行。 | ||
| 78 | + | ||
| 79 | +#### 3) `generate_model_visit(model) -> Generator[ProcessRequest, Any, None]` | ||
| 80 | + | ||
| 81 | +- **职责**:定义“按什么顺序遍历哪些模块”进行逐层处理。 | ||
| 82 | +- **输入**:初始化后的模型。 | ||
| 83 | +- **输出**:按顺序 `yield ProcessRequest`(每个 request 对应一个待处理模块)。 | ||
| 84 | +- **实现建议**:以真实 decoder/block 顺序输出,不跳层、不重排;名称路径应可唯一定位模块。 | ||
| 85 | +- **完成判定**:产出的层序列与 `generate_model_forward` 一一对应。 | ||
| 86 | + | ||
| 87 | +#### 4) `generate_model_forward(model, inputs) -> Generator[ProcessRequest, Any, None]` | ||
| 88 | + | ||
| 89 | +- **职责**:定义与 `visit` 对齐的分段前向,用于逐层校准。 | ||
| 90 | +- **输入**:模型 + 单条校准输入。 | ||
| 91 | +- **输出**:按顺序 `yield ProcessRequest`(包含该层执行所需输入)。 | ||
| 92 | +- **实现建议**:层顺序、分段边界、张量传递路径与 `generate_model_visit` 严格一致。 | ||
| 93 | +- **完成判定**:同一层在 visit/forward 的索引和语义完全匹配,不出现错位。 | ||
| 94 | + | ||
| 95 | +#### 5) `enable_kv_cache(model, need_kv_cache) -> None` | ||
| 96 | + | ||
| 97 | +- **职责**:统一控制 KV Cache 开关。 | ||
| 98 | +- **输入**:模型实例与布尔开关。 | ||
| 99 | +- **输出**:无返回(原地修改)。 | ||
| 100 | +- **实现建议**:优先复用基类 `_enable_kv_cache`;至少确保主干模型 config 中 `use_cache` 被正确设置。 | ||
| 101 | +- **完成判定**:开关后模型行为与预期一致,校准场景下通常可关闭以降低内存占用。 | ||
| 102 | + | ||
| 103 | +## 关键实现原则 | ||
| 104 | + | ||
| 105 | +### 1) `generate_model_visit` 与 `generate_model_forward` 必须严格一致 | ||
| 106 | + | ||
| 107 | +- 遍历层集合一致 | ||
| 108 | +- 顺序一致 | ||
| 109 | +- 分层输入输出传递一致 | ||
| 110 | + | ||
| 111 | +这是最容易出错、也最影响量化正确性的部分。 | ||
| 112 | + | ||
| 113 | +### 2) 不要靠模型名猜结构 | ||
| 114 | + | ||
| 115 | +必须以真实 `modeling` 代码为准,确认层路径、命名和 forward 行为后再写适配器。 | ||
| 116 | + | ||
| 117 | +### 3) VLM 只走“视觉整体 + 文本逐层” | ||
| 118 | + | ||
| 119 | +- 优先复用 VLM 基类 | ||
| 120 | +- visit/forward 中视觉模块与文本层顺序保持一致 | ||
| 121 | +- 图文融合逻辑需对齐目标模型官方 forward | ||
| 122 | + | ||
| 123 | +### 4) MoE 融合结构优先按“unpack 后纯线性层”适配 | ||
| 124 | + | ||
| 125 | +很多新模型的 MoE 使用融合/打包权重(常见为 3D 张量),而量化流程通常更适合 `nn.Linear` 形式的专家实现。 | ||
| 126 | + | ||
| 127 | +实现要求: | ||
| 128 | + | ||
| 129 | +- 先判断原始实现是否为 3D packed experts(不要假设所有 MoE 都一样) | ||
| 130 | +- 若是 packed 结构,不仅要在加载时 unpack,还要实现对应的 MoE 拆分 module | ||
| 131 | +- unpack 后专家应落到纯线性层(`gate_proj` / `up_proj` / `down_proj`),避免后续流程直接依赖 3D 权重 | ||
| 132 | +- 推荐结构:`moe_utils.py` 提供拆分后的 MoE module,`model_adapter.py` 负责权重 unpack 与模块替换 | ||
| 133 | + | ||
| 134 | +可参考 `qwen3_5` 的实现思路(`moe_utils.py`、`modeling_qwen3_5_mtp.py`):先识别 packed 权重,再拆分为逐 expert 线性层。 | ||
| 135 | + | ||
| 136 | +示例代码请参考: | ||
| 137 | + | ||
| 138 | +- `assets/moe_unpacked_module_example.py` | ||
| 139 | +- `assets/moe_unpacked_adapter_example.py` | ||
| 140 | + | ||
| 141 | +## 可选高阶特性 | ||
| 142 | + | ||
| 143 | +逐层量化(按层加载/懒加载)相关实现、工作流与示例见独立 Skill `adaptation/calibration/layer_wise`。 | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | +# 必需接口检查清单 | ||
| 2 | + | ||
| 3 | +在跑验证前,确认以下方法已实现并正确接入: | ||
| 4 | + | ||
| 5 | +- [ ] `handle_dataset` | ||
| 6 | +- [ ] `init_model` | ||
| 7 | +- [ ] `generate_model_visit` | ||
| 8 | +- [ ] `generate_model_forward` | ||
| 9 | +- [ ] `enable_kv_cache` | ||
| 10 | + | ||
| 11 | +## 对齐检查 | ||
| 12 | + | ||
| 13 | +- [ ] `generate_model_visit` 与 `generate_model_forward` 遍历的层一致 | ||
| 14 | +- [ ] 遍历顺序一致 | ||
| 15 | +- [ ] 层间输入输出传递一致 | ||
| 16 | +- [ ] 检查 tokenizer 的 `pad_token` / `pad_token_id`:若为 `None`,已在适配器中重写 `_load_tokenizer` 并设置 `pad_token = eos_token` | ||
| 17 | + | ||
| 18 | +## 注册检查 | ||
| 19 | + | ||
| 20 | +- [ ] `config/config.ini` 的 `[ModelAdapter]` 下已配置模型别名 | ||
| 21 | +- [ ] `config/config.ini` 的 `[ModelAdapterEntryPoints]` 下已配置入口 | ||
| 22 | +- [ ] 代码修改后已重新安装包 | ||
| @@ -0,0 +1,93 @@ | |||
| 1 | +# 模型适配基础接口参考 | ||
| 2 | + | ||
| 3 | +本文档只保留模型适配开发所需的基础接口。 | ||
| 4 | +不包含 SmoothQuant、QuaRot、FA3、FlatQuant 等高阶算法接口。 | ||
| 5 | + | ||
| 6 | +## 1) IModel(基础模型属性) | ||
| 7 | + | ||
| 8 | +**位置**: `msmodelslim/model/interface.py` | ||
| 9 | + | ||
| 10 | +所有适配器的基础属性接口: | ||
| 11 | + | ||
| 12 | +```python | ||
| 13 | +class IModel: | ||
| 14 | + @property | ||
| 15 | + def model_type(self) -> str | ||
| 16 | + | ||
| 17 | + @property | ||
| 18 | + def model_path(self) -> Path | ||
| 19 | + | ||
| 20 | + @property | ||
| 21 | + def trust_remote_code(self) -> bool | ||
| 22 | +``` | ||
| 23 | + | ||
| 24 | +实现要求: | ||
| 25 | + | ||
| 26 | +- `model_type`:返回模型类型标识。 | ||
| 27 | +- `model_path`:返回模型目录路径。 | ||
| 28 | +- `trust_remote_code`:返回是否允许远程代码。 | ||
| 29 | + | ||
| 30 | +## 2) ModelSlimPipelineInterfaceV1(必需) | ||
| 31 | + | ||
| 32 | +**位置**: `msmodelslim/core/runner/pipeline_interface.py` | ||
| 33 | + | ||
| 34 | +基础量化适配必须实现的核心接口: | ||
| 35 | + | ||
| 36 | +```python | ||
| 37 | +class PipelineInterface(IModel): | ||
| 38 | + @abstractmethod | ||
| 39 | + def handle_dataset(self, dataset: Any, device: DeviceType = DeviceType.NPU) -> List[Any]: | ||
| 40 | + ... | ||
| 41 | + | ||
| 42 | + @abstractmethod | ||
| 43 | + def init_model(self, device: DeviceType = DeviceType.NPU) -> nn.Module: | ||
| 44 | + ... | ||
| 45 | + | ||
| 46 | + @abstractmethod | ||
| 47 | + def generate_model_visit(self, model: nn.Module) -> Generator[ProcessRequest, Any, None]: | ||
| 48 | + ... | ||
| 49 | + | ||
| 50 | + @abstractmethod | ||
| 51 | + def generate_model_forward(self, model: nn.Module, inputs: Any) -> Generator[ProcessRequest, Any, None]: | ||
| 52 | + ... | ||
| 53 | + | ||
| 54 | + @abstractmethod | ||
| 55 | + def enable_kv_cache(self, model: nn.Module, need_kv_cache: bool) -> None: | ||
| 56 | + ... | ||
| 57 | +``` | ||
| 58 | + | ||
| 59 | +实现重点: | ||
| 60 | + | ||
| 61 | +- `generate_model_visit` 与 `generate_model_forward` 的层顺序必须严格一致。 | ||
| 62 | +- `handle_dataset` 输出必须可直接用于前向。 | ||
| 63 | +- `init_model` 返回可执行前向且可被逐层访问的模型。 | ||
| 64 | + | ||
| 65 | +## 3) ModelInfoInterface(推荐) | ||
| 66 | + | ||
| 67 | +**位置**: `msmodelslim/app/naive_quantization/model_info_interface.py` | ||
| 68 | +(部分场景也在 `msmodelslim/app/auto_tuning/model_info_interface.py` 使用) | ||
| 69 | + | ||
| 70 | +用于提供模型基础信息: | ||
| 71 | + | ||
| 72 | +```python | ||
| 73 | +def get_model_pedigree(self) -> str | ||
| 74 | +def get_model_type(self) -> str | ||
| 75 | +``` | ||
| 76 | + | ||
| 77 | +说明: | ||
| 78 | + | ||
| 79 | +- 该接口通常与 `TransformersModel + ModelSlimPipelineInterfaceV1` 组合使用。 | ||
| 80 | +- 若你的适配流程或导出流程依赖模型家族信息,建议实现。 | ||
| 81 | + | ||
| 82 | +## 推荐继承组合 | ||
| 83 | + | ||
| 84 | +基础模型适配(LLM/VLM 文本主干)建议: | ||
| 85 | + | ||
| 86 | +```python | ||
| 87 | +class MyModelAdapter(TransformersModel, | ||
| 88 | + ModelInfoInterface, | ||
| 89 | + ModelSlimPipelineInterfaceV1): | ||
| 90 | + pass | ||
| 91 | +``` | ||
| 92 | + | ||
| 93 | +若当前场景不需要模型信息能力,可省略 `ModelInfoInterface`,但 `ModelSlimPipelineInterfaceV1` 不可省略。 | ||
| @@ -0,0 +1,19 @@ | |||
| 1 | +# 模型结构分析指南 | ||
| 2 | + | ||
| 3 | +## 1. 确认模型结构来源 | ||
| 4 | + | ||
| 5 | +- 读取 `config.json` 的 `model_type`、`architectures`、`auto_map` | ||
| 6 | +- 确定结构来自模型仓内 `modeling_*.py` 还是 transformers 官方实现 | ||
| 7 | + | ||
| 8 | +## 2. 定位并阅读模型实现(必须) | ||
| 9 | + | ||
| 10 | +- **自定义实现**:如果 `auto_map` 指向自定义实现(如 `modeling_xxx.XXXForCausalLM`),优先阅读模型目录中的 `modeling_*.py` | ||
| 11 | +- **官方实现**:如果使用 transformers 官方实现,通常在: | ||
| 12 | + - 源码路径:`transformers/src/transformers/models/<model_type>/modeling_<model_type>.py` | ||
| 13 | + - 导入路径:`transformers.models.<model_type>.modeling_<model_type>` | ||
| 14 | +- **重点阅读**: | ||
| 15 | + - DecoderLayer 定义 | ||
| 16 | + - attention/MLP 命名 | ||
| 17 | + - `forward` 入参与返回值 | ||
| 18 | +- **MoE 模型额外检查**: | ||
| 19 | + - `experts` 权重是否为 3D packed 结构(如 `experts.gate_up_proj` / `experts.down_proj`) | ||
| @@ -0,0 +1,39 @@ | |||
| 1 | +# 适配器注册指南 | ||
| 2 | + | ||
| 3 | +在 `config/config.ini` 中注册模型与入口。 | ||
| 4 | + | ||
| 5 | +## 机制(setup.py entry point 如何生效) | ||
| 6 | + | ||
| 7 | +`install.sh` 走 `setup.py` 的 develop/editable 安装,把 `config.ini` 中的注册项生成为 Python **entry point**(console 插件),`msmodelslim` 运行期通过 `PluginModelFactory` 按 `--model_type` 的值查找插件: | ||
| 8 | + | ||
| 9 | +- `[ModelAdapter]`:`<适配组> = <型号值1>, <型号值2>, ...` —— 每个型号值就是用户 `--model_type` 会传入的字符串(如 `Qwen3-4B`)。同一适配组的多个型号共享同一套适配器实现。 | ||
| 10 | +- `[ModelAdapterEntryPoints]`:`<适配组> = <loader 的 python 导入路径>:<Loader 类名>` —— loader 指向键即适配组名(与 `[ModelAdapter]` 的组名一致);Loader 负责根据具体型号实例化对应的 `ModelAdapter`。 | ||
| 11 | +- `[ModelAdapterDependencies]`:`<适配组> = {"transformers": "==4.51.0"}` —— **语义是"依赖告警 + 包装",不是硬失败**:环境版本不匹配时通常告警并尝试继续(或做轻量兼容包装),不会因版本锁直接拒载。不要误以为必须精确装到锁的版本;也不要因为"只是告警"就无视,运行期异常往往由此而来。 | ||
| 12 | + | ||
| 13 | +## 示例 | ||
| 14 | + | ||
| 15 | +```ini | ||
| 16 | +[ModelAdapter] | ||
| 17 | +my_model = MyModel-7B, MyModel-13B | ||
| 18 | + | ||
| 19 | +[ModelAdapterEntryPoints] | ||
| 20 | +my_model = msmodelslim.model.my_model.loader:MyModelLoader | ||
| 21 | +``` | ||
| 22 | + | ||
| 23 | +## 注册 → 重装 → 验证配方 | ||
| 24 | + | ||
| 25 | +1. 新建适配器源码目录(如 `msmodelslim/model/my_model/`,含 `__init__.py`、`loader.py`、`model_adapter.py`)。 | ||
| 26 | +2. 在 `config/config.ini` 补上述三个小节(按需)的条目。 | ||
| 27 | +3. 执行 `bash install.sh` 重新安装,让 entry point 生效。 | ||
| 28 | +4. **验证:在仓库根目录之外**执行(源码树 import 遮蔽会假失败,见 installation 技能 F0-2): | ||
| 29 | + - 查看注册是否生效:列出 entry points,确认新适配器出现在列表中且数量 +1; | ||
| 30 | + - 负向对照:用一个未注册型号触发,确认回落 `default` 适配器(证明正向命中不是 default 兜底)。 | ||
| 31 | + | ||
| 32 | +## 适配面提示(V0 vs V1) | ||
| 33 | + | ||
| 34 | +`adaptation/calibration` 模板实现的是 `ModelSlimPipelineInterfaceV1` 五接口。同仓 dense 模型适配器普遍继承 `DefaultModelAdapter`,其中还含 **V0 兼容方法**(如 `load_model`、`handle_dataset_by_batch`)。是否补 V0 方法取决于下游走哪套配置: | ||
| 35 | + | ||
| 36 | +- 调优/量化走 `modelslim_v1`(apiversion 为 `modelslim_v1`)→ 只需 V1 五接口; | ||
| 37 | +- 若需消费 `modelslim_v0` 的 best-practice 配置(`lab_practice` 中的旧格式文件)或旧接口调用方 → 需补 V0 方法。 | ||
| 38 | + | ||
| 39 | +判断依据以实际要复用的配置/调用方为准,技能未在别处强制。 | ||
| @@ -0,0 +1,98 @@ | |||
| 1 | +--- | ||
| 2 | +name: verify | ||
| 3 | +description: 为 msModelSlim 适配器执行功能性验证。语义为减层结构覆盖(非全量):Step1 按结构类型生成覆盖完备的最小前缀测试模型,再跑全回退量化、权重一致性、W8A8 描述校验四步门禁。 | ||
| 4 | +metadata: | ||
| 5 | + # subagent 绑定声明:id = 委派标识(subagent_type);bind = 委派时须绑定加载的 skill 根目录(含 references/、scripts/) | ||
| 6 | + # 双重角色:① calibration 子任务的内部门禁(适配完成时自带四步验证);② 主 Agent 的适配验收门禁(NPU 前向推理判定基准) | ||
| 7 | + subagent: | ||
| 8 | + id: "adaptation/calibration/verify" | ||
| 9 | + bind: | ||
| 10 | + | ||
| 11 | + - "adaptation/calibration/verify" | ||
| 12 | + | ||
| 13 | +--- | ||
| 14 | + | ||
| 15 | +# 适配器功能性验证(verify) | ||
| 16 | + | ||
| 17 | +用于在基础适配器开发完成后,自动帮助用户进行功能性验证。 | ||
| 18 | + | ||
| 19 | +## 验证语义(硬约定) | ||
| 20 | + | ||
| 21 | +- **减层结构覆盖验证,不是全量验证。** | ||
| 22 | +- 大模型由若干**结构类型**(如 `attn:linear_attention` / `attn:full_attention` × `ffn:dense` / `ffn:moe`)堆叠而成;验证只需每种结构至少出现一次。 | ||
| 23 | +- Step1 默认取「覆盖完备的**最小前缀层数**」生成随机权重测试模型;**禁止**盲目前 N 层(会漏 MoE、full_attn 等)。 | ||
| 24 | +- **不要求**加载或量化原始全层权重;全量精度/性能属于后续量化调优,不在本 skill 范围。 | ||
| 25 | + | ||
| 26 | +## 触发条件 | ||
| 27 | + | ||
| 28 | +- `adaptation/calibration` 已完成适配器开发与注册安装。 | ||
| 29 | +- 用户希望确认适配器是否可用,或要求执行标准验证流程。 | ||
| 30 | + | ||
| 31 | +## 执行要求 | ||
| 32 | + | ||
| 33 | +- 必须按顺序执行四步验证,不可跳步。 | ||
| 34 | +- 每一步失败都要立即停止并返回失败原因与下一步修复建议。 | ||
| 35 | +- 仅当四步全部通过时,返回“功能性验证通过”。 | ||
| 36 | +- 修改代码后要执行 `bash install.sh` 重新安装msModelslim | ||
| 37 | +- 若验证过程中出现模型实现文件(如权重目录内 `modeling_*.py`)报错,必须先判断是否为 `transformers` 版本不契合导致。 | ||
| 38 | +- 对疑似版本不契合问题,必须先告知用户并确认版本需求(目标版本或可接受版本区间);未确认前不得切换版本。 | ||
| 39 | +- 仅在用户确认后,才可执行 `transformers` 版本切换与重试验证。 | ||
| 40 | + | ||
| 41 | +## 四步验证流程 | ||
| 42 | + | ||
| 43 | +1. Step1:按结构覆盖计划生成减层随机权重测试模型(见下方硬门禁)。 | ||
| 44 | +2. Step2:执行全回退量化,验证流程与注册生效。 | ||
| 45 | +3. Step3:验证 Step2 与 Step1 的权重严格一致,且产物可完整加载/保存。 | ||
| 46 | +4. Step4:执行实际量化(W8A8 静态/动态)并校验描述文件规则。 | ||
| 47 | + | ||
| 48 | +## 硬门禁:Step1 结构覆盖 | ||
| 49 | + | ||
| 50 | +- 执行前可用 `--plan-only` 打印覆盖计划;正式生成须落盘 `structure_cover_plan.json`。 | ||
| 51 | +- 覆盖完备:`incomplete=false`,且日志中每种结构标签均有「首次覆盖 layer[i]」。 | ||
| 52 | +- 若人为传入 `--num-layers` 导致漏盖 → **FAIL**(除非显式 `--allow-incomplete-cover`,仅调试)。 | ||
| 53 | +- 常见漏盖:MoE 从 layer≥K 才开始、hybrid attn 的 full 层按 interval 出现——前缀必须延伸到首次出现该类结构的层。 | ||
| 54 | + | ||
| 55 | +## Buffer 权重说明(Step3 常见问题) | ||
| 56 | + | ||
| 57 | +- 若 Step3 出现“全回退权重缺失/键不一致”,需优先检查缺失项是否来自模型 `buffer`。 | ||
| 58 | +- `msmodelslim` 通常不会保存 `buffer` 类型权重,因此可能导致全回退产物缺少对应键。 | ||
| 59 | +- 适配器需要主动将这类关键 `buffer` 转为 `nn.Parameter`,以确保量化导出和一致性校验可覆盖该权重。 | ||
| 60 | + | ||
| 61 | +## transformers 版本兼容处理(验证期) | ||
| 62 | + | ||
| 63 | +- 触发条件:验证阶段出现模型实现文件导入/模型forward错误,且报错指向 `transformers` API 变更、缺失符号或签名不匹配。 | ||
| 64 | +- 必做沟通:向用户说明“当前报错疑似版本兼容问题”、给出关键报错摘要、请求确认目标版本策略(指定版本或版本区间)。 | ||
| 65 | +- 搜索策略:获用户确认后,使用二分法在确认范围内搜索可用 `transformers` 版本(每次切换版本后需重装并重跑触发失败的验证步骤)。 | ||
| 66 | +- 收敛标准:找到“可成功加载并通过对应验证步骤”的版本后停止搜索,并将最终版本写入msModelslim的config.ini。 | ||
| 67 | +- 失败处理:若二分搜索后仍无可用版本,返回阻塞结论并要求用户提供官方建议版本或模型实现修订方案。 | ||
| 68 | + | ||
| 69 | +## 自动化脚本 | ||
| 70 | + | ||
| 71 | +- `scripts/step1_generate_test_model.py` | ||
| 72 | +- `scripts/step2_run_quantization.py` | ||
| 73 | +- `scripts/step3_verify_weights.py` | ||
| 74 | +- `scripts/step4_verify_quant_description.py` | ||
| 75 | + | ||
| 76 | +## 参考资料 | ||
| 77 | + | ||
| 78 | +- [适配器验证指南](references/verification_guide.md) | ||
| 79 | + | ||
| 80 | +## 输出格式要求 | ||
| 81 | + | ||
| 82 | +- 给出每一步的执行结果(PASS/FAIL)。 | ||
| 83 | +- 若失败,标注失败步骤、错误要点、建议修复方向。 | ||
| 84 | +- 最后给出总结结论:通过 / 未通过。 | ||
| 85 | + | ||
| 86 | +## 经验条目(Experiences,追加制) | ||
| 87 | + | ||
| 88 | +> 追加规范见 `skills/README.md`「经验条目」。连续编号 `[E-序号]`;正文保留权威展开,本表只做索引 + 元数据登记;来源:实测编号(F0-x / Dx / Tx)| 用户反馈 | 代码实证;验证状态:已回归 | 待验证 | 已上流 docs。 | ||
| 89 | + | ||
| 90 | +| 条目 | 主题 | 适用条件 / 触发信号 | 结论要点(一句话) | 正文位置 | 来源 | 验证状态 | | ||
| 91 | +|------|------|------|------|------|------|------| | ||
| 92 | +| E-001 | buffer 缺失优先自查 | Step3 权重缺失/键不一致 | 缺失项多来自模型 `buffer`(msmodelslim 通常不保存 buffer);适配器须将关键 buffer 主动转为 `nn.Parameter` 使其可被导出与校验覆盖 | Buffer 权重说明 | 代码实证 | 已回归 | | ||
| 93 | +| E-002 | tie 权重等价克隆豁免 | Step3 严格键集合比对 | tie 权重模型(如 Qwen3)量化产物会克隆 `lm_head.weight = embed_tokens.weight`,属等价克隆而非真不一致;比对前先判 shape+数值容差内相等再豁免 | Step3(脚本 `step3_verify_weights.py`) | 实测 G6 | 已回归 | | ||
| 94 | +| E-003 | transformers 版本兼容二分 | 验证期模型实现报错疑似版本不契合 | 先向用户说明并确认版本策略(目标版本或区间),确认后才二分搜索可用版本;收敛后把最终版本写入 config.ini;无可用版本返回阻塞并要官方建议 | transformers 版本兼容处理 | 用户反馈 | 待验证 | | ||
| 95 | +| E-004 | 修改代码后重装 | 任何 adapter 代码变更后 | 必须执行 `bash install.sh` 重新安装 msmodelslim 后再重试验证(脚本路径为 console 命令,非 `python -m msmodelslim`) | 执行要求 | 实测 G5 | 已回归 | | ||
| 96 | +| E-005 | 减层结构覆盖非全量 | Step1 生成测试模型 | 验证=结构覆盖完备的最小前缀减层,不是全量、也不是盲目前 N 层;MoE/hybrid 等晚出现结构必须被前缀覆盖 | 验证语义 / 硬门禁:Step1 结构覆盖 | 用户反馈 | 待验证 | | ||
| 97 | +| E-006 | 禁止盲目前 N 层 | `--num-layers` 过小 | 若上限导致 `incomplete`,step1 须 FAIL;扩大前缀或去掉上限,勿用 `--allow-incomplete-cover` 混过门禁 | 硬门禁:Step1 结构覆盖 | 用户反馈 | 待验证 | | ||
| 98 | +| E-007 | remote tied_weights list | Step1 `save_pretrained` AttributeError keys | 旧式 remote modeling 的 `_tied_weights_keys` 可能是 list;测试模型可清空为 `{}` 再保存 | Step1 脚本 | 实测 | 已回归 | | ||
| @@ -0,0 +1,20 @@ | |||
| 1 | +apiversion: modelslim_v1 | ||
| 2 | +spec: | ||
| 3 | + process: | ||
| 4 | + - type: "linear_quant" | ||
| 5 | + qconfig: | ||
| 6 | + act: | ||
| 7 | + scope: "per_token" | ||
| 8 | + dtype: "int8" | ||
| 9 | + symmetric: True | ||
| 10 | + method: "minmax" | ||
| 11 | + weight: | ||
| 12 | + scope: "per_channel" | ||
| 13 | + dtype: "int8" | ||
| 14 | + symmetric: True | ||
| 15 | + method: "minmax" | ||
| 16 | + include: ["*"] | ||
| 17 | + exclude: ["*"] # 全部回退,不实际量化任何层 | ||
| 18 | + save: | ||
| 19 | + - type: "ascendv1_saver" | ||
| 20 | + part_file_size: 4 | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | +apiversion: modelslim_v1 | ||
| 2 | + | ||
| 3 | +spec: | ||
| 4 | + process: | ||
| 5 | + - type: "linear_quant" | ||
| 6 | + qconfig: | ||
| 7 | + act: | ||
| 8 | + scope: "per_token" # dynamic | ||
| 9 | + dtype: "int8" | ||
| 10 | + symmetric: True | ||
| 11 | + method: "minmax" | ||
| 12 | + weight: | ||
| 13 | + scope: "per_channel" | ||
| 14 | + dtype: "int8" | ||
| 15 | + symmetric: True | ||
| 16 | + method: "minmax" | ||
| 17 | + include: ["*"] # 全模型量化 | ||
| 18 | + exclude: [] # 不回退 | ||
| 19 | + | ||
| 20 | + save: | ||
| 21 | + - type: "ascendv1_saver" | ||
| 22 | + part_file_size: 4 | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | +apiversion: modelslim_v1 | ||
| 2 | + | ||
| 3 | +spec: | ||
| 4 | + process: | ||
| 5 | + - type: "linear_quant" | ||
| 6 | + qconfig: | ||
| 7 | + act: | ||
| 8 | + scope: "per_tensor" # static | ||
| 9 | + dtype: "int8" | ||
| 10 | + symmetric: False | ||
| 11 | + method: "minmax" | ||
| 12 | + weight: | ||
| 13 | + scope: "per_channel" | ||
| 14 | + dtype: "int8" | ||
| 15 | + symmetric: True | ||
| 16 | + method: "minmax" | ||
| 17 | + include: ["*"] # 全模型量化 | ||
| 18 | + exclude: [] # 不回退 | ||
| 19 | + | ||
| 20 | + save: | ||
| 21 | + - type: "ascendv1_saver" | ||
| 22 | + part_file_size: 4 | ||
| @@ -0,0 +1,145 @@ | |||
| 1 | +# 适配器验证指南 | ||
| 2 | + | ||
| 3 | +## 验证语义 | ||
| 4 | + | ||
| 5 | +- **减层结构覆盖,不是全量。** 模型由结构类型堆叠;测试模型只需每种结构至少一层。 | ||
| 6 | +- Step1 按 `layer_types` ×(dense/MoE)计算覆盖完备的最小前缀层数,写入 `structure_cover_plan.json`。 | ||
| 7 | +- 后续 Step2~4 只在该减层模型上执行;不加载原始全层权重。 | ||
| 8 | + | ||
| 9 | +## 核心验证流程 (必须) | ||
| 10 | + | ||
| 11 | +必须按顺序执行以下四步验证: | ||
| 12 | + | ||
| 13 | +1. **生成测试模型** (Step 1) | ||
| 14 | + - 结构覆盖计划 → 减层 config → 随机权重小模型 | ||
| 15 | + - 门禁:覆盖完备(`incomplete=false`);禁止盲目前 N 层漏掉晚出现的 MoE/full_attn | ||
| 16 | +2. **全回退量化** (Step 2) | ||
| 17 | + - 验证量化流程是否能跑通(不涉及具体精度,仅跑通流程) | ||
| 18 | + - 检查 `model_adapter` 注册是否生效 | ||
| 19 | +3. **全回退模型一致性与可加载/保存验证** (Step 3) | ||
| 20 | + - 基于 Step2 生成的全回退模型,验证其与 Step1 浮点模型权重严格一致(键、形状、数值) | ||
| 21 | + - 验证该模型产物具备完整加载/保存能力(可被后续流程读取并继续处理) | ||
| 22 | + - 若发现全回退权重缺失,优先检查缺失项是否为模型 `buffer`(`msmodelslim` 默认不会保存 buffer 权重) | ||
| 23 | + - 对必须保留的 buffer 权重,需在适配器中主动转换为 `nn.Parameter` 后再参与验证 | ||
| 24 | +4. **实际量化流程验证** (Step 4) | ||
| 25 | + - 运行实际 W8A8 静态/动态量化流程(非回退流程)并产出量化结果 | ||
| 26 | + - 验证量化描述文件是否符合预期规则,检查线性层量化标签是否正确 | ||
| 27 | + | ||
| 28 | +## 验证命令 | ||
| 29 | + | ||
| 30 | +```bash | ||
| 31 | + | ||
| 32 | +# 1) 先看结构覆盖计划(推荐) | ||
| 33 | +python scripts/step1_generate_test_model.py \ | ||
| 34 | + --model-path /path/to/your/model \ | ||
| 35 | + --output-path /tmp/test_model \ | ||
| 36 | + --plan-only | ||
| 37 | + | ||
| 38 | +# 1') 生成减层测试模型(默认:覆盖完备最小前缀,勿再默认传 --num-layers 2) | ||
| 39 | +python scripts/step1_generate_test_model.py \ | ||
| 40 | + --model-path /path/to/your/model \ | ||
| 41 | + --output-path /tmp/test_model | ||
| 42 | + | ||
| 43 | +# 可选:设上限;若导致漏盖则失败 | ||
| 44 | + | ||
| 45 | +# --num-layers 8 | ||
| 46 | + | ||
| 47 | +# 调试才允许不完备: | ||
| 48 | + | ||
| 49 | +# --num-layers 2 --allow-incomplete-cover | ||
| 50 | + | ||
| 51 | +# 2) 全回退量化 | ||
| 52 | +python scripts/step2_run_quantization.py \ | ||
| 53 | + --model-path /tmp/test_model \ | ||
| 54 | + --output-path /tmp/quantized_model \ | ||
| 55 | + --model-type YourModelType \ | ||
| 56 | + --model-family llm | ||
| 57 | + | ||
| 58 | +# 多模态模型请使用: | ||
| 59 | + | ||
| 60 | +# --model-family vlm | ||
| 61 | + | ||
| 62 | +# 3) 全回退模型一致性验证(与浮点权重严格对齐) | ||
| 63 | +python scripts/step3_verify_weights.py \ | ||
| 64 | + --original-path /tmp/test_model \ | ||
| 65 | + --quantized-path /tmp/quantized_model \ | ||
| 66 | + --tolerance 1e-5 | ||
| 67 | +``` | ||
| 68 | + | ||
| 69 | +### Step 4:全模型量化检查 | ||
| 70 | + | ||
| 71 | +执行全模型 W8A8 静态量化并检查描述文件: | ||
| 72 | + | ||
| 73 | +```bash | ||
| 74 | + | ||
| 75 | +# 执行量化 | ||
| 76 | +msmodelslim quant \ | ||
| 77 | + --model_type <your_model_type> \ | ||
| 78 | + --model_path /tmp/test_model \ | ||
| 79 | + --save_path /tmp/quantized_w8a8_static \ | ||
| 80 | + --device cpu \ | ||
| 81 | + --config references/llm/w8a8_static_full_model.yaml \ | ||
| 82 | + --trust_remote_code True | ||
| 83 | + | ||
| 84 | +# 验证描述文件 | ||
| 85 | +python scripts/step4_verify_quant_description.py \ | ||
| 86 | + --desc-path /tmp/quantized_w8a8_static \ | ||
| 87 | + --rules-path /path/to/your_verify_rules_static.json | ||
| 88 | +``` | ||
| 89 | + | ||
| 90 | +执行全模型 W8A8 动态量化并检查描述文件: | ||
| 91 | + | ||
| 92 | +```bash | ||
| 93 | + | ||
| 94 | +# 执行量化 | ||
| 95 | +msmodelslim quant \ | ||
| 96 | + --model_type <your_model_type> \ | ||
| 97 | + --model_path /tmp/test_model \ | ||
| 98 | + --save_path /tmp/quantized_w8a8_dynamic \ | ||
| 99 | + --device cpu \ | ||
| 100 | + --config references/llm/w8a8_dynamic_full_model.yaml \ | ||
| 101 | + --trust_remote_code True | ||
| 102 | + | ||
| 103 | +# 验证描述文件 | ||
| 104 | +python scripts/step4_verify_quant_description.py \ | ||
| 105 | + --desc-path /tmp/quantized_w8a8_dynamic \ | ||
| 106 | + --rules-path /path/to/your_verify_rules_dynamic.json | ||
| 107 | +``` | ||
| 108 | + | ||
| 109 | +多模态模型(VLM)建议使用以下配置模板(含校准数据字段): | ||
| 110 | + | ||
| 111 | +```bash | ||
| 112 | +references/vlm/w8a8_static_full_model.yaml | ||
| 113 | +references/vlm/w8a8_dynamic_full_model.yaml | ||
| 114 | +``` | ||
| 115 | + | ||
| 116 | +说明:不再内置 `verify_rules_w8a8_static.json` / `verify_rules_w8a8_dynamic.json`,请 agent 按目标模型层名自行生成规则文件并传入 `--rules-path`。 | ||
| 117 | + | ||
| 118 | +## 通过标准 | ||
| 119 | + | ||
| 120 | +- **核心验证**:Step 1/2/3/4 均成功执行无报错。 | ||
| 121 | +- **Step 1 通过条件**:`structure_cover_plan.json` 存在且 `incomplete=false`;减层模型可加载保存。 | ||
| 122 | +- **Step 3 通过条件**:全回退模型与浮点模型权重检查 PASS,且量化产物可被后续流程正常加载/使用。 | ||
| 123 | +- **Step 4 通过条件**:实际量化流程执行成功,描述文件规则校验通过。 | ||
| 124 | + | ||
| 125 | +## 快速排错 / 失败分流 | ||
| 126 | + | ||
| 127 | +- **Step 1 失败**: | ||
| 128 | + - 结构覆盖不完备(exit 2):`--num-layers` 过小,漏掉晚出现的 MoE/attn 类型;去掉上限或增大层数后重跑 `--plan-only` 确认 | ||
| 129 | + - 模型加载失败:检查 `transformers` 版本或 `trust_remote_code` 设置 | ||
| 130 | + - `save_pretrained` 报 `_tied_weights_keys` / `list` 无 `.keys`:旧式 remote modeling;step1 脚本已对 list 清空为 dict,确认使用最新脚本 | ||
| 131 | + - 类型不支持:检查 `model_type` 是否在支持列表中 | ||
| 132 | +- **Step 2 失败**: | ||
| 133 | + - 找不到适配器:检查 `config.ini` 注册是否正确,是否执行了 `install.sh` | ||
| 134 | + - 量化入口报错:检查 `handle_dataset` 数据处理是否正确 | ||
| 135 | +- **Step 3 失败 (全回退模型与浮点不一致/不可完整加载)**: | ||
| 136 | + - 检查量化前后权重键名、形状与映射关系(应一一对应) | ||
| 137 | + - 检查数值差异是否超出阈值(默认 `tolerance=1e-5`) | ||
| 138 | + - 检查量化目录内权重与必要配置文件是否完整,确保可被后续流程读取 | ||
| 139 | + - 若报“缺少权重键”,检查该键在原模型中是否是 `buffer`;若是,需在适配器中将其转成 `nn.Parameter` | ||
| 140 | + - **MoE 模型**:若使用 packed 权重,检查 `packed -> unpacked` 拆分逻辑是否正确(维度、转置);并确认 Step1 前缀已覆盖至少一层 MoE | ||
| 141 | +- **Step 4 失败 (实际量化流程或描述文件异常)**: | ||
| 142 | + - 检查实际量化配置是否正确(W8A8 静态/动态、校准参数等) | ||
| 143 | + - 检查是否误用了回退配置 | ||
| 144 | + - 检查验证规则 JSON 中的关键字是否覆盖了模型实际层名;动态量化类型名为 `W8A8_DYNAMIC` | ||
| 145 | + - 规则应从产物 `quant_model_description.json` 中实际量化条目提取,勿把未 traverse 的 `lm_head`/`embed` 强行期望为 W8A8 | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | +apiversion: multimodal_vlm_modelslim_v1 | ||
| 2 | +spec: | ||
| 3 | + process: | ||
| 4 | + - type: "linear_quant" | ||
| 5 | + qconfig: | ||
| 6 | + act: | ||
| 7 | + scope: "per_token" | ||
| 8 | + dtype: "int8" | ||
| 9 | + symmetric: True | ||
| 10 | + method: "minmax" | ||
| 11 | + weight: | ||
| 12 | + scope: "per_channel" | ||
| 13 | + dtype: "int8" | ||
| 14 | + symmetric: True | ||
| 15 | + method: "minmax" | ||
| 16 | + include: ["*"] | ||
| 17 | + exclude: ["*"] # 全部回退,不实际量化任何层 | ||
| 18 | + save: | ||
| 19 | + - type: "ascendv1_saver" | ||
| 20 | + part_file_size: 4 | ||
| 21 | + dataset: "calibImages" | ||
| 22 | + default_text: "Describe this image in detail." | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | +apiversion: multimodal_vlm_modelslim_v1 | ||
| 2 | + | ||
| 3 | +spec: | ||
| 4 | + process: | ||
| 5 | + - type: "linear_quant" | ||
| 6 | + qconfig: | ||
| 7 | + act: | ||
| 8 | + scope: "per_token" # dynamic | ||
| 9 | + dtype: "int8" | ||
| 10 | + symmetric: True | ||
| 11 | + method: "minmax" | ||
| 12 | + weight: | ||
| 13 | + scope: "per_channel" | ||
| 14 | + dtype: "int8" | ||
| 15 | + symmetric: True | ||
| 16 | + method: "minmax" | ||
| 17 | + include: ["*"] # 全模型量化 | ||
| 18 | + exclude: [] # 不回退 | ||
| 19 | + | ||
| 20 | + save: | ||
| 21 | + - type: "ascendv1_saver" | ||
| 22 | + part_file_size: 4 | ||
| 23 | + | ||
| 24 | + # 多模态校准数据(需按实际数据集名称调整) | ||
| 25 | + dataset: "calibImages" | ||
| 26 | + default_text: "Describe this image in detail." | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | +apiversion: multimodal_vlm_modelslim_v1 | ||
| 2 | + | ||
| 3 | +spec: | ||
| 4 | + process: | ||
| 5 | + - type: "linear_quant" | ||
| 6 | + qconfig: | ||
| 7 | + act: | ||
| 8 | + scope: "per_tensor" # static | ||
| 9 | + dtype: "int8" | ||
| 10 | + symmetric: False | ||
| 11 | + method: "minmax" | ||
| 12 | + weight: | ||
| 13 | + scope: "per_channel" | ||
| 14 | + dtype: "int8" | ||
| 15 | + symmetric: True | ||
| 16 | + method: "minmax" | ||
| 17 | + include: ["*"] # 全模型量化 | ||
| 18 | + exclude: [] # 不回退 | ||
| 19 | + | ||
| 20 | + save: | ||
| 21 | + - type: "ascendv1_saver" | ||
| 22 | + part_file_size: 4 | ||
| 23 | + | ||
| 24 | + # 多模态校准数据(需按实际数据集名称调整) | ||
| 25 | + dataset: "calibImages" | ||
| 26 | + default_text: "Describe this image in detail." | ||
| @@ -0,0 +1,297 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +"""步骤1:生成结构覆盖完备的减层随机权重测试模型。 | ||
| 3 | + | ||
| 4 | +验证语义(非全量): | ||
| 5 | + 模型由若干结构类型堆叠而成;减层只需覆盖每种结构至少一次。 | ||
| 6 | + 默认取「覆盖完备的最小前缀层数」L,再 from_config 建随机权重模型。 | ||
| 7 | + 禁止盲目前 N 层(可能漏 MoE / full_attn 等)。 | ||
| 8 | +""" | ||
| 9 | + | ||
| 10 | +from __future__ import annotations | ||
| 11 | + | ||
| 12 | +import argparse | ||
| 13 | +import json | ||
| 14 | +import os | ||
| 15 | +import shutil | ||
| 16 | +import sys | ||
| 17 | +from typing import Any, Dict, List, Optional, Set, Tuple | ||
| 18 | + | ||
| 19 | +import torch | ||
| 20 | +from transformers import AutoConfig | ||
| 21 | +import transformers | ||
| 22 | + | ||
| 23 | +# 配置中按层对齐的 list 字段(减层时一并截断) | ||
| 24 | +_PER_LAYER_LIST_KEYS = ( | ||
| 25 | + "layer_types", | ||
| 26 | + "swiglu_limits", | ||
| 27 | + "swiglu_limits_shared", | ||
| 28 | +) | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +def _read_json(path: str) -> dict: | ||
| 32 | + with open(path, "r", encoding="utf-8") as f: | ||
| 33 | + return json.load(f) | ||
| 34 | + | ||
| 35 | + | ||
| 36 | +def _write_json(path: str, data: dict) -> None: | ||
| 37 | + with open(path, "w", encoding="utf-8") as f: | ||
| 38 | + json.dump(data, f, ensure_ascii=False, indent=2) | ||
| 39 | + | ||
| 40 | + | ||
| 41 | +def _copy_non_weight_files(src_dir: str, dst_dir: str) -> None: | ||
| 42 | + os.makedirs(dst_dir, exist_ok=True) | ||
| 43 | + for name in os.listdir(src_dir): | ||
| 44 | + src = os.path.join(src_dir, name) | ||
| 45 | + dst = os.path.join(dst_dir, name) | ||
| 46 | + if os.path.isdir(src): | ||
| 47 | + continue | ||
| 48 | + if name.endswith(".safetensors"): | ||
| 49 | + continue | ||
| 50 | + if name.endswith(".index.json"): | ||
| 51 | + continue | ||
| 52 | + shutil.copy2(src, dst) | ||
| 53 | + | ||
| 54 | + | ||
| 55 | +def _text_cfg_view(cfg: dict) -> Tuple[dict, bool]: | ||
| 56 | + """返回 (text_cfg_dict, is_nested_under_text_config)。""" | ||
| 57 | + if isinstance(cfg.get("text_config"), dict): | ||
| 58 | + return cfg["text_config"], True | ||
| 59 | + return cfg, False | ||
| 60 | + | ||
| 61 | + | ||
| 62 | +def _parse_moe_layer_indices(text_cfg: dict, n_layers: int) -> Set[int]: | ||
| 63 | + """从 config 解析 MoE 层下标集合;无 MoE 信号则返回空集。""" | ||
| 64 | + enum = text_cfg.get("moe_layers_enum") | ||
| 65 | + if enum is not None: | ||
| 66 | + if isinstance(enum, str): | ||
| 67 | + return {int(x) for x in enum.split(",") if str(x).strip() != ""} | ||
| 68 | + if isinstance(enum, (list, tuple)): | ||
| 69 | + return {int(x) for x in enum} | ||
| 70 | + if not text_cfg.get("use_moe") and not text_cfg.get("num_experts") and not text_cfg.get("moe_num_experts"): | ||
| 71 | + return set() | ||
| 72 | + # 常见回退:除第 0 层外均为 MoE,或按 moe_every_n_layer / moe_layer_offset | ||
| 73 | + every = text_cfg.get("moe_every_n_layer") | ||
| 74 | + offset = int(text_cfg.get("moe_layer_offset") or 0) | ||
| 75 | + if every: | ||
| 76 | + every = int(every) | ||
| 77 | + return {i for i in range(n_layers) if i >= offset and (i - offset) % every == 0} | ||
| 78 | + # DeepSeek 类:首层 dense,其余 MoE | ||
| 79 | + return set(range(1, n_layers)) | ||
| 80 | + | ||
| 81 | + | ||
| 82 | +def _structure_label(layer_idx: int, layer_types: Optional[List[Any]], moe_indices: Set[int]) -> str: | ||
| 83 | + """单层结构标签:注意力类型 × FFN 类型(堆叠重复则标签相同)。""" | ||
| 84 | + if layer_types and layer_idx < len(layer_types): | ||
| 85 | + attn = f"attn:{layer_types[layer_idx]}" | ||
| 86 | + else: | ||
| 87 | + attn = "attn:default" | ||
| 88 | + if moe_indices: | ||
| 89 | + ffn = "ffn:moe" if layer_idx in moe_indices else "ffn:dense" | ||
| 90 | + else: | ||
| 91 | + ffn = "ffn:dense" | ||
| 92 | + return f"{attn}+{ffn}" | ||
| 93 | + | ||
| 94 | + | ||
| 95 | +def plan_structure_cover( | ||
| 96 | + cfg: dict, | ||
| 97 | + min_layers: int = 1, | ||
| 98 | + max_layers: Optional[int] = None, | ||
| 99 | +) -> Dict[str, Any]: | ||
| 100 | + """计算结构覆盖完备的最小前缀层数。 | ||
| 101 | + | ||
| 102 | + Returns: | ||
| 103 | + dict: cover_layers, first_occurrence, all_labels, incomplete (若 max 截断导致漏盖) | ||
| 104 | + """ | ||
| 105 | + text_cfg, _ = _text_cfg_view(cfg) | ||
| 106 | + n_layers = int(text_cfg.get("num_hidden_layers") or 1) | ||
| 107 | + layer_types = text_cfg.get("layer_types") | ||
| 108 | + if not isinstance(layer_types, list): | ||
| 109 | + layer_types = None | ||
| 110 | + moe_indices = _parse_moe_layer_indices(text_cfg, n_layers) | ||
| 111 | + | ||
| 112 | + first_occ: Dict[str, int] = {} | ||
| 113 | + for i in range(n_layers): | ||
| 114 | + label = _structure_label(i, layer_types, moe_indices) | ||
| 115 | + if label not in first_occ: | ||
| 116 | + first_occ[label] = i | ||
| 117 | + | ||
| 118 | + if not first_occ: | ||
| 119 | + cover = max(min_layers, 1) | ||
| 120 | + return { | ||
| 121 | + "cover_layers": cover, | ||
| 122 | + "first_occurrence": {}, | ||
| 123 | + "all_labels": [], | ||
| 124 | + "incomplete": False, | ||
| 125 | + "original_layers": n_layers, | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + needed = max(first_occ.values()) + 1 | ||
| 129 | + cover = max(needed, min_layers) | ||
| 130 | + incomplete = False | ||
| 131 | + missing: List[str] = [] | ||
| 132 | + if max_layers is not None and cover > max_layers: | ||
| 133 | + # 在 max 前缀内仍未出现的标签 → 不完备 | ||
| 134 | + covered_in_max = {lab for lab, idx in first_occ.items() if idx < max_layers} | ||
| 135 | + missing = sorted(set(first_occ) - covered_in_max) | ||
| 136 | + cover = max_layers | ||
| 137 | + incomplete = bool(missing) | ||
| 138 | + | ||
| 139 | + return { | ||
| 140 | + "cover_layers": cover, | ||
| 141 | + "first_occurrence": first_occ, | ||
| 142 | + "all_labels": sorted(first_occ.keys()), | ||
| 143 | + "incomplete": incomplete, | ||
| 144 | + "missing_labels": missing, | ||
| 145 | + "original_layers": n_layers, | ||
| 146 | + "moe_layer_count": len(moe_indices), | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + | ||
| 150 | +def _truncate_per_layer_lists(text_cfg: dict, num_layers: int) -> dict: | ||
| 151 | + out = dict(text_cfg) | ||
| 152 | + out["num_hidden_layers"] = num_layers | ||
| 153 | + for key in _PER_LAYER_LIST_KEYS: | ||
| 154 | + val = out.get(key) | ||
| 155 | + if isinstance(val, list): | ||
| 156 | + out[key] = val[:num_layers] | ||
| 157 | + # moe_layers_enum:过滤到减层范围内,保持原类型(str / list) | ||
| 158 | + enum = out.get("moe_layers_enum") | ||
| 159 | + if isinstance(enum, str): | ||
| 160 | + kept = [x.strip() for x in enum.split(",") if x.strip() and int(x) < num_layers] | ||
| 161 | + out["moe_layers_enum"] = ",".join(kept) | ||
| 162 | + elif isinstance(enum, list): | ||
| 163 | + out["moe_layers_enum"] = [int(x) for x in enum if int(x) < num_layers] | ||
| 164 | + return out | ||
| 165 | + | ||
| 166 | + | ||
| 167 | +def _shrink_config(cfg: dict, num_layers: int) -> dict: | ||
| 168 | + """按结构覆盖层数减层(前缀截断 + 同步 per-layer / moe 枚举)。""" | ||
| 169 | + out = dict(cfg) | ||
| 170 | + text_cfg, nested = _text_cfg_view(out) | ||
| 171 | + shrunk = _truncate_per_layer_lists(text_cfg, num_layers) | ||
| 172 | + if nested: | ||
| 173 | + out["text_config"] = shrunk | ||
| 174 | + else: | ||
| 175 | + out.update(shrunk) | ||
| 176 | + return out | ||
| 177 | + | ||
| 178 | + | ||
| 179 | +def _build_random_model_from_config(config): | ||
| 180 | + candidate_auto_model_names = [ | ||
| 181 | + "AutoModelForCausalLM", | ||
| 182 | + "AutoModelForImageTextToText", | ||
| 183 | + "AutoModel", | ||
| 184 | + ] | ||
| 185 | + errors = [] | ||
| 186 | + for cls_name in candidate_auto_model_names: | ||
| 187 | + auto_cls = getattr(transformers, cls_name, None) | ||
| 188 | + if auto_cls is None: | ||
| 189 | + continue | ||
| 190 | + try: | ||
| 191 | + model = auto_cls.from_config(config, trust_remote_code=True, torch_dtype=torch.float32) | ||
| 192 | + return model, cls_name | ||
| 193 | + except Exception as e: # pragma: no cover - best-effort fallback chain | ||
| 194 | + errors.append(f"{cls_name}: {repr(e)}") | ||
| 195 | + | ||
| 196 | + raise RuntimeError( | ||
| 197 | + "无法根据配置构建模型。已尝试: " + ", ".join(candidate_auto_model_names) + "\n错误详情:\n" + "\n".join(errors) | ||
| 198 | + ) | ||
| 199 | + | ||
| 200 | + | ||
| 201 | +def main() -> int: | ||
| 202 | + parser = argparse.ArgumentParser(description="Step1: 结构覆盖完备的减层随机权重测试模型(非全量验证)") | ||
| 203 | + parser.add_argument("--model-path", required=True) | ||
| 204 | + parser.add_argument("--output-path", required=True) | ||
| 205 | + parser.add_argument( | ||
| 206 | + "--num-layers", | ||
| 207 | + type=int, | ||
| 208 | + default=None, | ||
| 209 | + help="可选:减层上限。默认不设上限,取结构覆盖完备的最小前缀。" | ||
| 210 | + "若上限导致结构覆盖不完备则失败(除非 --allow-incomplete-cover)。", | ||
| 211 | + ) | ||
| 212 | + parser.add_argument( | ||
| 213 | + "--min-layers", | ||
| 214 | + type=int, | ||
| 215 | + default=1, | ||
| 216 | + help="减层下限(默认 1)。结构种类少于此时仍至少保留该层数。", | ||
| 217 | + ) | ||
| 218 | + parser.add_argument( | ||
| 219 | + "--allow-incomplete-cover", | ||
| 220 | + action="store_true", | ||
| 221 | + help="允许在 --num-layers 截断后结构覆盖不完备(不推荐;仅调试)。", | ||
| 222 | + ) | ||
| 223 | + parser.add_argument( | ||
| 224 | + "--plan-only", | ||
| 225 | + action="store_true", | ||
| 226 | + help="只打印结构覆盖计划,不生成模型。", | ||
| 227 | + ) | ||
| 228 | + parser.add_argument("--device", default="cpu") | ||
| 229 | + args = parser.parse_args() | ||
| 230 | + | ||
| 231 | + src_cfg = os.path.join(args.model_path, "config.json") | ||
| 232 | + if not os.path.exists(src_cfg): | ||
| 233 | + print(f"[ERROR] 缺少配置文件: {src_cfg}") | ||
| 234 | + return 1 | ||
| 235 | + | ||
| 236 | + cfg = _read_json(src_cfg) | ||
| 237 | + plan = plan_structure_cover(cfg, min_layers=args.min_layers, max_layers=args.num_layers) | ||
| 238 | + | ||
| 239 | + print("[INFO] 验证语义: 减层结构覆盖(非全量)") | ||
| 240 | + print(f"[INFO] 原层数: {plan['original_layers']} → 覆盖层数: {plan['cover_layers']}") | ||
| 241 | + print(f"[INFO] 结构标签全集 ({len(plan['all_labels'])}): {plan['all_labels']}") | ||
| 242 | + for lab, idx in sorted(plan["first_occurrence"].items(), key=lambda x: x[1]): | ||
| 243 | + print(f"[INFO] 首次覆盖 layer[{idx}] = {lab}") | ||
| 244 | + if plan.get("moe_layer_count"): | ||
| 245 | + print(f"[INFO] MoE 层计数(原配置): {plan['moe_layer_count']}") | ||
| 246 | + | ||
| 247 | + if plan["incomplete"]: | ||
| 248 | + msg = ( | ||
| 249 | + f"结构覆盖不完备: --num-layers={args.num_layers} 漏掉 {plan.get('missing_labels')}. " | ||
| 250 | + "请去掉上限或增大 --num-layers,使前缀覆盖全部结构标签。" | ||
| 251 | + ) | ||
| 252 | + if args.allow_incomplete_cover: | ||
| 253 | + print(f"[WARN] {msg} (已 --allow-incomplete-cover,继续)") | ||
| 254 | + else: | ||
| 255 | + print(f"[ERROR] {msg}") | ||
| 256 | + return 2 | ||
| 257 | + | ||
| 258 | + if args.plan_only: | ||
| 259 | + print("[OK] plan-only 完成") | ||
| 260 | + return 0 | ||
| 261 | + | ||
| 262 | + _copy_non_weight_files(args.model_path, args.output_path) | ||
| 263 | + shrunk = _shrink_config(cfg, plan["cover_layers"]) | ||
| 264 | + _write_json(os.path.join(args.output_path, "config.json"), shrunk) | ||
| 265 | + # 落盘覆盖计划,供 step2~4 / 评审引用 | ||
| 266 | + _write_json( | ||
| 267 | + os.path.join(args.output_path, "structure_cover_plan.json"), | ||
| 268 | + { | ||
| 269 | + "semantics": "reduced_layer_structure_cover", | ||
| 270 | + "not_full_model": True, | ||
| 271 | + **plan, | ||
| 272 | + }, | ||
| 273 | + ) | ||
| 274 | + | ||
| 275 | + config = AutoConfig.from_pretrained(args.output_path, trust_remote_code=True) | ||
| 276 | + model, used_cls_name = _build_random_model_from_config(config) | ||
| 277 | + print(f"[INFO] 使用模型类: {used_cls_name}") | ||
| 278 | + model = model.to(args.device).eval() | ||
| 279 | + | ||
| 280 | + # remote-code 兼容:部分旧式 modeling 的 `_tied_weights_keys` 是 list, | ||
| 281 | + # transformers>=5.5 期望 dict(见 modeling_utils._get_tied_weight_keys 内 .keys())。 | ||
| 282 | + # 测试模型无 tie 精度诉求,统一清空避免 save_pretrained 崩溃。 | ||
| 283 | + for _, submodule in model.named_modules(): | ||
| 284 | + tied = getattr(submodule, "_tied_weights_keys", None) | ||
| 285 | + if isinstance(tied, list): | ||
| 286 | + submodule._tied_weights_keys = {} | ||
| 287 | + | ||
| 288 | + model.save_pretrained(args.output_path) | ||
| 289 | + stale_index = os.path.join(args.output_path, "model.safetensors.index.json") | ||
| 290 | + if os.path.exists(stale_index) and os.path.exists(os.path.join(args.output_path, "model.safetensors")): | ||
| 291 | + os.remove(stale_index) | ||
| 292 | + print(f"[OK] step1完成: {args.output_path} (cover_layers={plan['cover_layers']}, labels={len(plan['all_labels'])})") | ||
| 293 | + return 0 | ||
| 294 | + | ||
| 295 | + | ||
| 296 | +if __name__ == "__main__": | ||
| 297 | + sys.exit(main()) | ||
| @@ -0,0 +1,105 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +"""步骤2:执行全回退量化(精简版,支持 LLM/VLM)。""" | ||
| 3 | + | ||
| 4 | +import argparse | ||
| 5 | +import os | ||
| 6 | +import subprocess # nosec B404 - 执行 msmodelslim quant 是 skill 的本质需求 | ||
| 7 | +import sys | ||
| 8 | + | ||
| 9 | + | ||
| 10 | +LLM_FALLBACK_YAML = """apiversion: modelslim_v1 | ||
| 11 | +spec: | ||
| 12 | + process: | ||
| 13 | + - type: "linear_quant" | ||
| 14 | + qconfig: | ||
| 15 | + act: | ||
| 16 | + scope: "per_token" | ||
| 17 | + dtype: "int8" | ||
| 18 | + symmetric: True | ||
| 19 | + method: "minmax" | ||
| 20 | + weight: | ||
| 21 | + scope: "per_channel" | ||
| 22 | + dtype: "int8" | ||
| 23 | + symmetric: True | ||
| 24 | + method: "minmax" | ||
| 25 | + include: ["*"] | ||
| 26 | + exclude: ["*"] | ||
| 27 | + save: | ||
| 28 | + - type: "ascendv1_saver" | ||
| 29 | + part_file_size: 4 | ||
| 30 | +""" | ||
| 31 | + | ||
| 32 | + | ||
| 33 | +VLM_FALLBACK_YAML = """apiversion: multimodal_vlm_modelslim_v1 | ||
| 34 | +spec: | ||
| 35 | + process: | ||
| 36 | + - type: "linear_quant" | ||
| 37 | + qconfig: | ||
| 38 | + act: | ||
| 39 | + scope: "per_token" | ||
| 40 | + dtype: "int8" | ||
| 41 | + symmetric: True | ||
| 42 | + method: "minmax" | ||
| 43 | + weight: | ||
| 44 | + scope: "per_channel" | ||
| 45 | + dtype: "int8" | ||
| 46 | + symmetric: True | ||
| 47 | + method: "minmax" | ||
| 48 | + include: ["*"] | ||
| 49 | + exclude: ["*"] | ||
| 50 | + save: | ||
| 51 | + - type: "ascendv1_saver" | ||
| 52 | + part_file_size: 4 | ||
| 53 | + dataset: "calibImages" | ||
| 54 | + default_text: "Describe this image in detail." | ||
| 55 | +""" | ||
| 56 | + | ||
| 57 | + | ||
| 58 | +def _write_fallback_yaml(path, model_family: str): | ||
| 59 | + os.makedirs(os.path.dirname(path) or ".", exist_ok=True) | ||
| 60 | + content = VLM_FALLBACK_YAML if model_family == "vlm" else LLM_FALLBACK_YAML | ||
| 61 | + with open(path, "w", encoding="utf-8") as f: | ||
| 62 | + f.write(content) | ||
| 63 | + | ||
| 64 | + | ||
| 65 | +def main(): | ||
| 66 | + parser = argparse.ArgumentParser() | ||
| 67 | + parser.add_argument("--model-path", required=True) | ||
| 68 | + parser.add_argument("--output-path", required=True) | ||
| 69 | + parser.add_argument("--model-type", required=True) | ||
| 70 | + parser.add_argument("--device", default="cpu") | ||
| 71 | + parser.add_argument("--config-path", default="") | ||
| 72 | + parser.add_argument("--model-family", choices=["llm", "vlm"], default="llm") | ||
| 73 | + args = parser.parse_args() | ||
| 74 | + | ||
| 75 | + config_path = args.config_path or os.path.join(args.output_path, "fallback_config.yaml") | ||
| 76 | + if not os.path.exists(config_path): | ||
| 77 | + _write_fallback_yaml(config_path, args.model_family) | ||
| 78 | + | ||
| 79 | + os.makedirs(args.output_path, exist_ok=True) | ||
| 80 | + cmd = [ | ||
| 81 | + "msmodelslim", # console script:本包无顶层 __main__.py,不能用 python -m msmodelslim | ||
| 82 | + "quant", | ||
| 83 | + "--model_path", | ||
| 84 | + args.model_path, | ||
| 85 | + "--save_path", | ||
| 86 | + args.output_path, | ||
| 87 | + "--device", | ||
| 88 | + args.device, | ||
| 89 | + "--model_type", | ||
| 90 | + args.model_type, | ||
| 91 | + "--config", | ||
| 92 | + config_path, | ||
| 93 | + "--trust_remote_code", | ||
| 94 | + "True", | ||
| 95 | + ] | ||
| 96 | + rc = subprocess.run(cmd, check=False).returncode # nosec B603 - 命令为固定参数列表,无 shell 注入面 | ||
| 97 | + if rc != 0: | ||
| 98 | + print("[ERROR] step2失败") | ||
| 99 | + return rc | ||
| 100 | + print(f"[OK] step2完成: {args.output_path}") | ||
| 101 | + return 0 | ||
| 102 | + | ||
| 103 | + | ||
| 104 | +if __name__ == "__main__": | ||
| 105 | + sys.exit(main()) | ||
| @@ -0,0 +1,115 @@ | |||
| 1 | +#!/usr/bin/env python3 | ||
| 2 | +"""步骤3:验证权重一致性(精简版,支持 tie-weight 等价克隆豁免)。""" | ||
| 3 | + | ||
| 4 | +import argparse | ||
| 5 | +import glob | ||
| 6 | +import os | ||
| 7 | +import sys | ||
| 8 | + | ||
| 9 | +import torch | ||
| 10 | + | ||
| 11 | + | ||
| 12 | +def _load_weights(model_path): | ||
| 13 | + try: | ||
| 14 | + from safetensors.torch import load_file | ||
| 15 | + | ||
| 16 | + files = sorted(glob.glob(os.path.join(model_path, "*.safetensors"))) | ||
| 17 | + if files: | ||
| 18 | + merged = {} | ||
| 19 | + for file in files: | ||
| 20 | + merged.update(load_file(file)) | ||
| 21 | + return merged | ||
| 22 | + except Exception: # nosec B110 - safetensors 缺失时回退到 pytorch_model.bin | ||
| 23 | + pass | ||
| 24 | + | ||
| 25 | + pt_path = os.path.join(model_path, "pytorch_model.bin") | ||
| 26 | + if os.path.exists(pt_path): | ||
| 27 | + return torch.load(pt_path, map_location="cpu", weights_only=True) | ||
| 28 | + return {} | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +def _find_equivalent(key, source_map, target_map, tolerance): | ||
| 32 | + """在 target_map 中寻找与 source_map[key] 同 shape 且数值全等的克隆键。 | ||
| 33 | + | ||
| 34 | + tie-word-embeddings 模型(如 Qwen3):msmodelslim saver 有意把 embed_tokens.weight | ||
| 35 | + 克隆为 lm_head.weight 以兼容推理,而 transformers 原模型只保存一份 —— 严格键集 | ||
| 36 | + 比对会对这类模型误报 FAIL。数值全等(max diff <= tolerance)即视为等价克隆,属可接受差异。 | ||
| 37 | + """ | ||
| 38 | + target_tensor = source_map[key] | ||
| 39 | + for cand_key in target_map: | ||
| 40 | + cand = target_map[cand_key] | ||
| 41 | + if cand.shape != target_tensor.shape: | ||
| 42 | + continue | ||
| 43 | + if torch.abs(cand.float() - target_tensor.float()).max().item() <= tolerance: | ||
| 44 | + return cand_key | ||
| 45 | + return None | ||
| 46 | + | ||
| 47 | + | ||
| 48 | +def main(): | ||
| 49 | + parser = argparse.ArgumentParser() | ||
| 50 | + parser.add_argument("--original-path", required=True) | ||
| 51 | + parser.add_argument("--quantized-path", required=True) | ||
| 52 | + parser.add_argument("--tolerance", type=float, default=1e-5) | ||
| 53 | + args = parser.parse_args() | ||
| 54 | + | ||
| 55 | + left = _load_weights(args.original_path) | ||
| 56 | + right = _load_weights(args.quantized_path) | ||
| 57 | + if not left or not right: | ||
| 58 | + print("[ERROR] step3失败: 权重加载失败") | ||
| 59 | + return 1 | ||
| 60 | + | ||
| 61 | + left_keys, right_keys = set(left.keys()), set(right.keys()) | ||
| 62 | + | ||
| 63 | + # 1) 公共键直接纳入数值比较;单侧键先尝试等价克隆豁免,无克隆对应才算真不一致 | ||
| 64 | + pairs = [(k, k) for k in sorted(left_keys & right_keys)] | ||
| 65 | + accepted_clones = [] # (单侧键, 对应克隆键, 方向说明) | ||
| 66 | + missing = [] # 仅左侧且无克隆对应(真缺失) | ||
| 67 | + unexpected = [] # 仅右侧且无克隆对应(真多余) | ||
| 68 | + | ||
| 69 | + for key in sorted(left_keys - right_keys): | ||
| 70 | + eq = _find_equivalent(key, left, right, args.tolerance) | ||
| 71 | + if eq is not None: | ||
| 72 | + accepted_clones.append((key, eq, "quantized 侧含数值全等克隆")) | ||
| 73 | + pairs.append((key, eq)) | ||
| 74 | + else: | ||
| 75 | + missing.append(key) | ||
| 76 | + for key in sorted(right_keys - left_keys): | ||
| 77 | + eq = _find_equivalent(key, right, left, args.tolerance) | ||
| 78 | + if eq is not None: | ||
| 79 | + accepted_clones.append((key, eq, "original 侧含数值全等克隆")) | ||
| 80 | + pairs.append((eq, key)) | ||
| 81 | + else: | ||
| 82 | + unexpected.append(key) | ||
| 83 | + | ||
| 84 | + if missing or unexpected: | ||
| 85 | + print("[ERROR] step3失败: 权重键不一致(已排除等价克隆)") | ||
| 86 | + print(f"[INFO] 仅左侧数量(真缺失): {len(missing)}") | ||
| 87 | + for k in missing[:10]: | ||
| 88 | + print(f" - {k}") | ||
| 89 | + print(f"[INFO] 仅右侧数量(真多余): {len(unexpected)}") | ||
| 90 | + for k in unexpected[:10]: | ||
| 91 | + print(f" - {k}") | ||
| 92 | + return 1 | ||
| 93 | + | ||
| 94 | + # 2) 数值一致性:公共键 + 被豁免的克隆配对 | ||
| 95 | + max_diff = 0.0 | ||
| 96 | + for left_key, right_key in pairs: | ||
| 97 | + left_t = left[left_key] | ||
| 98 | + right_t = right[right_key] | ||
| 99 | + if left_t.shape != right_t.shape: | ||
| 100 | + print(f"[ERROR] step3失败: 形状不一致 {left_key} vs {right_key}") | ||
| 101 | + return 1 | ||
| 102 | + diff = torch.abs(left_t.float() - right_t.float()).max().item() | ||
| 103 | + max_diff = max(max_diff, diff) | ||
| 104 | + if diff > args.tolerance: | ||
| 105 | + print(f"[ERROR] step3失败: 权重差异超阈值 {left_key} diff={diff:.2e}") | ||
| 106 | + return 1 | ||
| 107 | + | ||
| 108 | + print(f"[OK] step3完成: max_diff={max_diff:.2e}, 等价克隆豁免 {len(accepted_clones)} 项") | ||
| 109 | + for key, eq, why in accepted_clones: | ||
| 110 | + print(f"[INFO] 豁免: {key} == {eq} ({why})") | ||
| 111 | + return 0 | ||
| 112 | + | ||
| 113 | + | ||
| 114 | +if __name__ == "__main__": | ||
| 115 | + sys.exit(main()) | ||
| @@ -0,0 +1,135 @@ | |||
| 1 | +#!/usr/bin/env python | ||
| 2 | +# -*- coding: UTF-8 -*- | ||
| 3 | +""" | ||
| 4 | +验证流程步骤4:验证量化描述文件 | ||
| 5 | +根据规则文件检查 quant_model_description.json 中的层量化类型是否符合预期。 | ||
| 6 | +""" | ||
| 7 | + | ||
| 8 | +import os | ||
| 9 | +import sys | ||
| 10 | +import json | ||
| 11 | +import argparse | ||
| 12 | +from typing import Any | ||
| 13 | + | ||
| 14 | + | ||
| 15 | +def load_json(path: str) -> Any: | ||
| 16 | + with open(path, "r", encoding="utf-8") as f: | ||
| 17 | + return json.load(f) | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +def find_description_file(path: str) -> str: | ||
| 21 | + """在指定路径查找描述文件""" | ||
| 22 | + if os.path.isfile(path): | ||
| 23 | + return path | ||
| 24 | + | ||
| 25 | + p = os.path.join(path, "quant_model_description.json") | ||
| 26 | + | ||
| 27 | + if os.path.exists(p): | ||
| 28 | + return p | ||
| 29 | + | ||
| 30 | + return None | ||
| 31 | + | ||
| 32 | + | ||
| 33 | +def verify_description(desc_path: str, rules_path: str) -> bool: | ||
| 34 | + print("=" * 60) | ||
| 35 | + print("步骤4: 验证量化描述文件") | ||
| 36 | + print("=" * 60) | ||
| 37 | + | ||
| 38 | + real_desc_path = find_description_file(desc_path) | ||
| 39 | + if not real_desc_path: | ||
| 40 | + print(f"[ERROR] 未找到量化描述文件 (在路径: {desc_path})") | ||
| 41 | + print(" 期望文件: quant_model_description.json 或 quant_model_description.json") | ||
| 42 | + return False | ||
| 43 | + | ||
| 44 | + print(f"[INFO] 描述文件: {real_desc_path}") | ||
| 45 | + try: | ||
| 46 | + desc_data = load_json(real_desc_path) | ||
| 47 | + except Exception as e: | ||
| 48 | + print(f"[ERROR] 加载描述文件失败: {e}") | ||
| 49 | + return False | ||
| 50 | + | ||
| 51 | + if not isinstance(desc_data, dict): | ||
| 52 | + print("[ERROR] 描述文件格式错误: 期望为 JSON Object (dict)") | ||
| 53 | + return False | ||
| 54 | + | ||
| 55 | + print(f"[INFO] 规则文件: {rules_path}") | ||
| 56 | + if not os.path.exists(rules_path): | ||
| 57 | + print(f"[ERROR] 规则文件不存在: {rules_path}") | ||
| 58 | + return False | ||
| 59 | + | ||
| 60 | + try: | ||
| 61 | + rules = load_json(rules_path) | ||
| 62 | + except Exception as e: | ||
| 63 | + print(f"[ERROR] 加载规则文件失败: {e}") | ||
| 64 | + return False | ||
| 65 | + | ||
| 66 | + if not isinstance(rules, list): | ||
| 67 | + print("[ERROR] 规则文件格式错误: 期望为 JSON Array (list)") | ||
| 68 | + return False | ||
| 69 | + | ||
| 70 | + print("\n[CHECK] 开始匹配规则...") | ||
| 71 | + all_passed = True | ||
| 72 | + total_checked_keys = 0 | ||
| 73 | + | ||
| 74 | + for i, rule in enumerate(rules): | ||
| 75 | + quant_type = rule.get("quant_type") | ||
| 76 | + keywords = rule.get("keywords", []) | ||
| 77 | + | ||
| 78 | + if not quant_type or not keywords: | ||
| 79 | + print(f"[WARNING] 规则 #{i + 1} 格式无效 (缺少 quant_type 或 keywords),跳过") | ||
| 80 | + continue | ||
| 81 | + | ||
| 82 | + print(f" > 规则 #{i + 1}: 期望包含 {keywords} 的权重为 '{quant_type}'") | ||
| 83 | + | ||
| 84 | + matched_keys = [] | ||
| 85 | + failed_keys = [] | ||
| 86 | + | ||
| 87 | + for key, value in desc_data.items(): | ||
| 88 | + if not isinstance(key, str): | ||
| 89 | + continue | ||
| 90 | + | ||
| 91 | + is_match = any(kw in key for kw in keywords) | ||
| 92 | + if is_match: | ||
| 93 | + if value != quant_type: | ||
| 94 | + failed_keys.append((key, value)) | ||
| 95 | + else: | ||
| 96 | + matched_keys.append(key) | ||
| 97 | + | ||
| 98 | + total_checked_keys += len(matched_keys) + len(failed_keys) | ||
| 99 | + | ||
| 100 | + if failed_keys: | ||
| 101 | + all_passed = False | ||
| 102 | + print(f" [FAILED] 发现 {len(failed_keys)} 个不匹配项 (展示前10个):") | ||
| 103 | + for k, v in failed_keys[:10]: | ||
| 104 | + print(f" - {k}: 实际值='{v}', 期望值='{quant_type}'") | ||
| 105 | + if len(failed_keys) > 10: | ||
| 106 | + print(f" ... 还有 {len(failed_keys) - 10} 个") | ||
| 107 | + elif not matched_keys: | ||
| 108 | + print(" [WARNING] 未找到匹配该规则关键字的任何权重键 (可能是关键字有误?)") | ||
| 109 | + else: | ||
| 110 | + print(f" [OK] {len(matched_keys)} 个权重项验证通过") | ||
| 111 | + | ||
| 112 | + print("-" * 60) | ||
| 113 | + if all_passed and total_checked_keys > 0: | ||
| 114 | + print("[SUCCESS] 验证通过!所有匹配项均符合预期量化类型。") | ||
| 115 | + return True | ||
| 116 | + if total_checked_keys == 0: | ||
| 117 | + print("[FAILED] 验证失败:未匹配到任何符合规则的权重项,请检查规则关键字。") | ||
| 118 | + return False | ||
| 119 | + print("[FAILED] 验证失败:存在量化类型不匹配的权重项。") | ||
| 120 | + return False | ||
| 121 | + | ||
| 122 | + | ||
| 123 | +def main(): | ||
| 124 | + parser = argparse.ArgumentParser(description="验证量化描述文件内容") | ||
| 125 | + parser.add_argument("--desc-path", required=True, help="量化输出目录或描述文件路径") | ||
| 126 | + parser.add_argument("--rules-path", required=True, help="校验规则JSON文件路径") | ||
| 127 | + | ||
| 128 | + args = parser.parse_args() | ||
| 129 | + | ||
| 130 | + success = verify_description(args.desc_path, args.rules_path) | ||
| 131 | + sys.exit(0 if success else 1) | ||
| 132 | + | ||
| 133 | + | ||
| 134 | +if __name__ == "__main__": | ||
| 135 | + main() | ||
| @@ -0,0 +1,22 @@ | |||
| 1 | +apiversion: multimodal_vlm_modelslim_v1 | ||
| 2 | +spec: | ||
| 3 | + process: | ||
| 4 | + - type: "linear_quant" | ||
| 5 | + qconfig: | ||
| 6 | + act: | ||
| 7 | + scope: "per_token" | ||
| 8 | + dtype: "int8" | ||
| 9 | + symmetric: True | ||
| 10 | + method: "minmax" | ||
| 11 | + weight: | ||
| 12 | + scope: "per_channel" | ||
| 13 | + dtype: "int8" | ||
| 14 | + symmetric: True | ||
| 15 | + method: "minmax" | ||
| 16 | + include: ["*"] | ||
| 17 | + exclude: ["*"] # 全部回退,不实际量化任何层 | ||
| 18 | + save: | ||
| 19 | + - type: "ascendv1_saver" | ||
| 20 | + part_file_size: 4 | ||
| 21 | + dataset: "calibImages" | ||
| 22 | + default_text: "Describe this image in detail." | ||
| @@ -0,0 +1,118 @@ | |||
| 1 | +#!/usr/bin/env python | ||
| 2 | +# -*- coding: UTF-8 -*- | ||
| 3 | + | ||
| 4 | +""" | ||
| 5 | +多模态理解模型(VLM)适配器模板。 | ||
| 6 | +""" | ||
| 7 | + | ||
| 8 | +from pathlib import Path | ||
| 9 | +from typing import Any, Generator, List | ||
| 10 | + | ||
| 11 | +from torch import nn | ||
| 12 | +from transformers import AutoProcessor | ||
| 13 | + | ||
| 14 | +from msmodelslim.app.naive_quantization.model_info_interface import ModelInfoInterface | ||
| 15 | +from msmodelslim.core.base.protocol import ProcessRequest | ||
| 16 | +from msmodelslim.core.const import DeviceType | ||
| 17 | +from msmodelslim.model.common.layer_wise_forward import ( | ||
| 18 | + generated_decoder_layer_visit_func, | ||
| 19 | + transformers_generated_forward_func, | ||
| 20 | +) | ||
| 21 | +from msmodelslim.model.common.vlm_base import VLMBaseModelAdapter | ||
| 22 | +from msmodelslim.model.interface_hub import ModelSlimPipelineInterfaceV1 | ||
| 23 | +from msmodelslim.utils.exception import InvalidModelError | ||
| 24 | +from msmodelslim.utils.logging import logger_setter | ||
| 25 | +from msmodelslim.utils.security import get_valid_read_path | ||
| 26 | + | ||
| 27 | + | ||
| 28 | + | ||
| 29 | +class MyVLMModelAdapter( | ||
| 30 | + VLMBaseModelAdapter, | ||
| 31 | + ModelInfoInterface, | ||
| 32 | + ModelSlimPipelineInterfaceV1, | ||
| 33 | +): | ||
| 34 | + """VLM 基础模板。""" | ||
| 35 | + | ||
| 36 | + def __init__(self, model_type: str, model_path: Path, trust_remote_code: bool = False): | ||
| 37 | + self._processor = None | ||
| 38 | + super().__init__(model_type, model_path, trust_remote_code) | ||
| 39 | + | ||
| 40 | + # ==================== ModelInfoInterface ==================== | ||
| 41 | + def get_model_pedigree(self) -> str: | ||
| 42 | + return "my_vlm" | ||
| 43 | + | ||
| 44 | + def get_model_type(self) -> str: | ||
| 45 | + return self.model_type | ||
| 46 | + | ||
| 47 | + # ==================== ModelSlimPipelineInterfaceV1 ==================== | ||
| 48 | + def handle_dataset(self, dataset: Any, device: DeviceType = DeviceType.NPU) -> List[Any]: | ||
| 49 | + """ | ||
| 50 | + 将图文样本转换为校准输入。 | ||
| 51 | + 样本建议格式:item.text + item.image(或 dict: {"text": ..., "image": ...})。 | ||
| 52 | + """ | ||
| 53 | + self._processor = AutoProcessor.from_pretrained( # nosec B615 - local_files_only=True 仅加载本地模型,不触发远程下载 | ||
| 54 | + self.model_path, | ||
| 55 | + trust_remote_code=self.trust_remote_code, | ||
| 56 | + local_files_only=True, | ||
| 57 | + ) | ||
| 58 | + processed = [] | ||
| 59 | + for item in dataset: | ||
| 60 | + text = item.text if hasattr(item, "text") else item.get("text") | ||
| 61 | + image = item.image if hasattr(item, "image") else item.get("image") | ||
| 62 | + if text is None or image is None: | ||
| 63 | + raise InvalidModelError( | ||
| 64 | + "VLM 校准样本需要同时包含 text 和 image。", | ||
| 65 | + action="请提供 image+text 数据,避免纯文本样本。", | ||
| 66 | + ) | ||
| 67 | + | ||
| 68 | + image = get_valid_read_path(str(image)) | ||
| 69 | + messages = [ | ||
| 70 | + { | ||
| 71 | + "role": "user", | ||
| 72 | + "content": [ | ||
| 73 | + {"type": "image", "image": str(image)}, | ||
| 74 | + {"type": "text", "text": str(text)}, | ||
| 75 | + ], | ||
| 76 | + } | ||
| 77 | + ] | ||
| 78 | + inputs = self._processor.apply_chat_template( | ||
| 79 | + messages, | ||
| 80 | + tokenize=True, | ||
| 81 | + add_generation_prompt=True, | ||
| 82 | + return_dict=True, | ||
| 83 | + return_tensors="pt", | ||
| 84 | + ) | ||
| 85 | + processed.append( | ||
| 86 | + self._collect_inputs_to_device( | ||
| 87 | + inputs, | ||
| 88 | + device, | ||
| 89 | + keys=[ | ||
| 90 | + "input_ids", | ||
| 91 | + "attention_mask", | ||
| 92 | + "position_ids", | ||
| 93 | + "pixel_values", | ||
| 94 | + "pixel_values_videos", | ||
| 95 | + "image_grid_thw", | ||
| 96 | + "video_grid_thw", | ||
| 97 | + "cache_position", | ||
| 98 | + ], | ||
| 99 | + defaults={}, | ||
| 100 | + ) | ||
| 101 | + ) | ||
| 102 | + return processed | ||
| 103 | + | ||
| 104 | + def init_model(self, device: DeviceType = DeviceType.NPU) -> nn.Module: | ||
| 105 | + return self._load_model(device) | ||
| 106 | + | ||
| 107 | + def _load_model(self, device: DeviceType) -> nn.Module: | ||
| 108 | + """Placeholder: implement VLM loading (see e.g. qwen2_5_vl adapter).""" | ||
| 109 | + raise NotImplementedError("Implement _load_model for the target VLM.") | ||
| 110 | + | ||
| 111 | + def generate_model_visit(self, model: nn.Module) -> Generator[ProcessRequest, Any, None]: | ||
| 112 | + yield from generated_decoder_layer_visit_func(model) | ||
| 113 | + | ||
| 114 | + def generate_model_forward(self, model: nn.Module, inputs: Any) -> Generator[ProcessRequest, Any, None]: | ||
| 115 | + yield from transformers_generated_forward_func(model, inputs) | ||
| 116 | + | ||
| 117 | + def enable_kv_cache(self, model: nn.Module, need_kv_cache: bool) -> None: | ||
| 118 | + return self._enable_kv_cache(model, need_kv_cache) | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | +apiversion: multimodal_vlm_modelslim_v1 | ||
| 2 | + | ||
| 3 | +spec: | ||
| 4 | + process: | ||
| 5 | + - type: "linear_quant" | ||
| 6 | + qconfig: | ||
| 7 | + act: | ||
| 8 | + scope: "per_token" # dynamic | ||
| 9 | + dtype: "int8" | ||
| 10 | + symmetric: True | ||
| 11 | + method: "minmax" | ||
| 12 | + weight: | ||
| 13 | + scope: "per_channel" | ||
| 14 | + dtype: "int8" | ||
| 15 | + symmetric: True | ||
| 16 | + method: "minmax" | ||
| 17 | + include: ["*"] # 全模型量化 | ||
| 18 | + exclude: [] # 不回退 | ||
| 19 | + | ||
| 20 | + save: | ||
| 21 | + - type: "ascendv1_saver" | ||
| 22 | + part_file_size: 4 | ||
| 23 | + | ||
| 24 | + # 多模态校准数据(需按实际数据集名称调整) | ||
| 25 | + dataset: "calibImages" | ||
| 26 | + default_text: "Describe this image in detail." | ||
| @@ -0,0 +1,26 @@ | |||
| 1 | +apiversion: multimodal_vlm_modelslim_v1 | ||
| 2 | + | ||
| 3 | +spec: | ||
| 4 | + process: | ||
| 5 | + - type: "linear_quant" | ||
| 6 | + qconfig: | ||
| 7 | + act: | ||
| 8 | + scope: "per_tensor" # static | ||
| 9 | + dtype: "int8" | ||
| 10 | + symmetric: False | ||
| 11 | + method: "minmax" | ||
| 12 | + weight: | ||
| 13 | + scope: "per_channel" | ||
| 14 | + dtype: "int8" | ||
| 15 | + symmetric: True | ||
| 16 | + method: "minmax" | ||
| 17 | + include: ["*"] # 全模型量化 | ||
| 18 | + exclude: [] # 不回退 | ||
| 19 | + | ||
| 20 | + save: | ||
| 21 | + - type: "ascendv1_saver" | ||
| 22 | + part_file_size: 4 | ||
| 23 | + | ||
| 24 | + # 多模态校准数据(需按实际数据集名称调整) | ||
| 25 | + dataset: "calibImages" | ||
| 26 | + default_text: "Describe this image in detail." | ||