---
name: vllm-ascend-offload-param-adaptation
description: |
  使用 AI 编码助手完成 vLLM Ascend 权重 offload 参数适配的开发方法论。聚焦
  `offload_group_size` / `offload_num_in_group` 的参数调研、语义建模、vLLM
  offloader 接口接入、Ascend 同步 offload 与预取 offload 实现、runner 集成、测试护栏
  和代码审查要点。

  适用场景：
  - 在 vLLM Ascend / vLLM OOT 后端中适配上游已有但后端未充分实现的权重 offload 参数
  - 需要把通用 vLLM `BaseOffloader` / `set_offloader()` 机制接入 Ascend NPU 模型加载路径
  - 需要把参数语义先沉淀为可单测纯函数，再扩展到同步和预取执行路径
  - 需要审查 AI 生成的硬件后端代码，避免默认路径回归、参数语义漂移和异步预取生命周期错误

  使用时机：
  - 用户要求详细调研 `offload_group_size` / `offload_num_in_group` 的语义和实现方案
  - 用户要求开发 Ascend 权重 offload 参数，并解释关键代码路径
  - 用户要求为每个参数补测试，并解释 baseline、sync fallback、prefetch 实现之间的代码关系
---

# vLLM Ascend 权重 Offload 参数适配 — AI 协作 Skills

> 赛道: CCF 开源创新大赛 vLLM Ascend 参数适配专项赛  
> 适配参数: `offload_group_size` + `offload_num_in_group`  
> 核心代码: `vllm_ascend/model_executor/offloader/*` + `worker/model_runner_v1.py`  
> AI 工具: Codex / GPT-5  
> 日期: 2026-07-05

## 场景识别

根据用户当前所处阶段，阅读对应章节：

**参数调研** — 读 §1（语义钉牢法）
- 用户要求弄清 `offload_group_size` / `offload_num_in_group` 到底控制什么
- 用户需要判断默认值、非法值、边界条件和 layer 选择方向
- 用户需要把自然语言参数说明转成可执行、可测试的纯函数

**上游链路分析** — 读 §2（Config → Offloader → Runner 链路追踪）
- 用户需要确认 vLLM 的 offloader 抽象如何工作
- 用户需要找出 vLLM Ascend 中最小侵入的接入点
- 用户需要避免把参数逻辑散落到模型层或脚本层

**代码实现** — 读 §3（Noop → sync → prefetch 三段式）
- 用户要新增 Ascend 权重 offloader
- 用户要同时提供保守同步实现和默认预取实现
- 用户需要处理 CPU tensor、static buffer、NPU stream/event 和 forward hook 生命周期

**Runner 集成** — 读 §4（单一 factory 接入法）
- 用户要在 `NPUModelRunner` 中设置 offloader
- 用户需要保证默认行为不变，非法参数启动时报错
- 用户需要通过环境变量在 sync/prefetch 间切换

**测试与审查** — 读 §5（参数级测试护栏）
- 用户要求每个参数都有独立测试
- 用户要审查 AI 生成代码是否存在语义漂移、异步生命周期和默认路径回归
- 用户要在真实模型 benchmark 前先用无设备单测锁住核心逻辑

通用知识见 §0。每章末尾的“失败案例”来自本次开发过程，重点记录 AI 容易写错或写偏的地方。

---

## §0 通用知识

### 0.1 本次适配的代码边界

本 skill 只覆盖开发代码相关内容，重点是参数语义、offloader 实现、runner 集成与测试护栏。

| 类型 | 路径 | 职责 |
|---|---|---|
| 参数选择 | `vllm_ascend/model_executor/offloader/selection.py` | 把 group 参数转换为 offload layer index |
| 同步实现 | `vllm_ascend/model_executor/offloader/sync.py` | forward 前 onload、forward 后 offload |
| 预取实现 | `vllm_ascend/model_executor/offloader/prefetch.py` | static buffer + NPU stream/event 预取 |
| 模块导出 | `vllm_ascend/model_executor/offloader/__init__.py` | 暴露 offloader 与选择函数 |
| Runner 集成 | `vllm_ascend/worker/model_runner_v1.py` | 创建并注册 Ascend offloader |
| 单元测试 | `tests/model_executor/offloader/*.py` | 覆盖选择逻辑、sync、prefetch、runner 集成 |

### 0.2 最终参数语义

最终代码以 `selection.py` 为准：

```python
cutoff = group_size - num_in_group
indices = tuple(
    layer_idx
    for layer_idx in range(num_layers)
    if layer_idx % group_size >= cutoff
)
```

等价描述：

```text
offload if layer_idx % offload_group_size >= offload_group_size - offload_num_in_group
```

含义：每个 group 中选择**末尾 N 层**进行 offload。

示例：

| num_layers | group_size | num_in_group | selected layer indices |
|---:|---:|---:|---|
| 8 | 4 | 1 | `(3, 7)` |
| 8 | 4 | 2 | `(2, 3, 6, 7)` |
| 8 | 2 | 1 | `(1, 3, 5, 7)` |
| 7 | 3 | 1 | `(2, 5)` |

边界行为：

| 条件 | 行为 |
|---|---|
| `group_size == 0` | 禁用 offload |
| `num_in_group == 0` | 禁用 offload |
| `num_layers <= 0` | 禁用 offload |
| `num_in_group > group_size` | 抛出 `ValueError` |
| `group_size < 0` | 抛出 `ValueError` |
| `num_in_group < 0` | 抛出 `ValueError` |

### 0.3 AI 协作核心心法

**心法 1：参数语义先写成纯函数。**  
硬件后端代码复杂，但参数选择本身不应该依赖 NPU。先把语义落到 `select_offload_layers()`，再让 sync/prefetch 复用它。

**心法 2：默认路径必须先被证明是 Noop。**  
`offload_group_size=0` 是最重要的兼容性开关。任何实现都必须保证默认参数不 wrap module、不迁移参数、不改变 forward。

**心法 3：先做同步正确路径，再做异步预取路径。**  
`sync` 是调试参照物，`prefetch` 是优化路径。没有 sync fallback，预取 bug 会和参数选择 bug 混在一起。

**心法 4：文档描述必须从最终源码回填。**  
本次开发中早期草稿曾写成“每组前 N 层”，最终源码是“每组末尾 N 层”。最终 skill 和测试必须重新读取源码，不能沿用早期记忆。

---

## §1 参数调研：语义钉牢法

### 1.1 调研目标

调研阶段不要先写 offloader。先回答 5 个问题：

1. `offload_group_size` 的单位是什么？
2. `offload_num_in_group` 与 group size 的关系是什么？
3. 默认值如何保持原行为？
4. 参数非法时应该静默 Noop 还是显式报错？
5. layer 选择顺序是每组前 N 层还是末尾 N 层？

本次最终答案：

| 问题 | 结论 |
|---|---|
| 单位 | decoder layer group |
| 关系 | `num_in_group <= group_size` |
| 默认 | `group_size=0` 或 `num_in_group=0` 均 Noop |
| 非法 | 负数或 `num_in_group > group_size` 报错 |
| 选择方向 | 每组末尾 N 层 |

### 1.2 把自然语言转成判定表

先写判定表，再写代码：

| case | 输入 | 预期 |
|---|---|---|
| disabled by group | `num_layers=10, group_size=0, num_in_group=1` | disabled |
| disabled by num | `num_layers=10, group_size=4, num_in_group=0` | disabled |
| exact groups | `num_layers=8, group_size=4, num_in_group=1` | `(3, 7)` |
| multiple in group | `num_layers=8, group_size=4, num_in_group=2` | `(2, 3, 6, 7)` |
| partial final group | `num_layers=7, group_size=3, num_in_group=1` | `(2, 5)` |
| invalid M>N | `group_size=2, num_in_group=3` | `ValueError` |

这个表直接驱动 `test_selection.py`、`test_offload_group_size_param.py` 和 `test_offload_num_in_group_param.py`。

### 1.3 纯函数设计

使用不可变返回值，避免下游修改选择结果：

```python
from dataclasses import dataclass

@dataclass(frozen=True)
class OffloadSelection:
    enabled: bool
    layer_indices: tuple[int, ...]
```

选择函数不接触 torch、不接触 model、不接触 NPU：

```python
def select_offload_layers(
    *,
    num_layers: int,
    group_size: int,
    num_in_group: int,
) -> OffloadSelection:
    ...
```

这样 AI 生成的硬件相关代码即使出错，参数语义仍有单独测试保护。

### 1.4 调研 Prompt 模板

```text
任务：调研 offload_group_size / offload_num_in_group 的准确语义。

请不要写 offloader 代码。先输出：
1. 参数单位和配置来源
2. 默认值对应行为
3. 非法参数组合
4. layer 选择公式
5. 至少 6 个输入/输出样例
6. 应该落在哪个纯函数中

要求：样例必须覆盖 group_size=0、num_in_group=0、M>N、部分 final group。
```

### 1.5 失败案例：早期描述与最终选择方向不一致

**问题**：早期材料曾描述为“每组前 N 层 offload”。但最终实现是：

```python
layer_idx % group_size >= group_size - num_in_group
```

这表示“每组末尾 N 层”。

**修正**：

1. 以 `selection.py` 为唯一语义源。
2. 在 skill 中加入具体样例，例如 `group_size=4,num_in_group=1 -> (3,7)`。
3. 测试文件必须断言这些样例，而不是只断言数量。

**教训**：AI 会记住早期自然语言草稿。最终开发文档必须从代码和测试反推，而不是从对话记忆反推。

---

## §2 上游链路分析：Config → Offloader → Runner 链路追踪

### 2.1 需要追踪的抽象

vLLM 已有通用 offloader 抽象，Ascend 侧不应重新发明一套全局机制：

| 抽象 | 作用 |
|---|---|
| `BaseOffloader` | 定义 offloader 生命周期接口 |
| `set_offloader()` | 设置当前全局 offloader |
| `get_offloader()` | 模型加载路径读取当前 offloader |
| `NoopOffloader` | 默认不做任何 offload |
| `OffloadConfig` | 持有 offload 参数 |

调研时让 AI 追踪的是“参数如何到达模型加载前”，而不是“哪里方便就在哪里 monkey patch”。

### 2.2 Ascend 侧最小接入点

本次选择在 `vllm_ascend/worker/model_runner_v1.py` 增加：

```python
create_ascend_weight_offloader(offload_config)
```

并在 runner 初始化或模型加载前调用：

```python
set_offloader(create_ascend_weight_offloader(self.offload_config))
```

选择这个接入点的原因：

1. runner 已持有 `offload_config`
2. 模型加载前设置 offloader，后续 module wrapping 才能生效
3. 对模型实现侵入小，不需要修改每个 model class
4. 默认 Noop 可在 factory 中统一处理

### 2.3 文件级差异矩阵

| 文件 | 调研结论 | 实现动作 |
|---|---|---|
| `model_runner_v1.py` | runner 是 offload config 到 model load 的交界 | 增加 factory、校验、`set_offloader()` |
| `selection.py` | 参数语义可独立于硬件 | 新增 `select_offload_layers()` |
| `sync.py` | 需要保守正确路径 | 新增 `AscendSyncOffloader` |
| `prefetch.py` | 需要默认优化路径 | 新增 `AscendPrefetchOffloader` |
| `__init__.py` | offloader 模块需要统一导出 | 导出 selection/sync/prefetch |

### 2.4 链路追踪 Prompt 模板

```text
任务：追踪 vLLM offloader 在 Ascend runner 中的接入点。

请输出：
1. OffloadConfig 在 runner 中的可见位置
2. set_offloader / get_offloader 的调用时序
3. 模型 layer 何时会被 wrap
4. 最小修改点
5. 不应该修改的层面，例如具体 model class 或 benchmark script

输出格式：Config -> Runner -> Offloader -> Module wrap 的链路图 + 文件级修改表。
```

### 2.5 失败案例：把参数逻辑写进执行脚本

**问题**：为了快速验证，AI 容易把 group 参数和 layer 选择写进 benchmark 或 smoke 脚本。这能跑通，但不是后端能力。

**修正**：脚本只传参数和收集结果；参数语义必须在 `selection.py`，执行策略必须在 `sync.py` / `prefetch.py`，接入必须在 runner。

**教训**：参数适配的标准是“用户通过正常 vLLM Ascend 配置即可触发能力”，不是“某个实验脚本能手工模拟能力”。

---

## §3 代码实现：Noop → sync → prefetch 三段式

### 3.1 第一段：Noop 与选择逻辑

先实现 `selection.py`：

```python
if group_size == 0 or num_in_group == 0 or num_layers <= 0:
    return OffloadSelection(enabled=False, layer_indices=())
if num_in_group > group_size:
    raise ValueError(...)
```

这一段的完成标准：

- 不 import torch
- 不依赖 NPU
- 不读取环境变量
- 不修改 module
- 所有边界都有单测

### 3.2 第二段：同步 offloader

`AscendSyncOffloader` 的职责是提供“慢但直观”的正确路径。

核心对象：

```python
@dataclass
class _ParamState:
    name: str
    param: nn.Parameter
    cpu_tensor: torch.Tensor
    original_device: torch.device
    num_bytes: int
```

生命周期：

1. `wrap_modules()` 收集所有 decoder layer
2. 调用 `select_offload_layers()`
3. 对选中 layer 创建 `_SyncModuleOffloader`
4. `_capture_params()` 把参数复制到 CPU contiguous tensor
5. forward 前 `_onload()` 把参数搬回运行设备
6. forward 后 `_offload()` 恢复 CPU tensor
7. `post_init()` 统计 `total_offloaded_bytes`

关键实现点：

```python
def _install_forward_hook(self) -> None:
    def forward(*args, **kwargs):
        self._onload(args, kwargs)
        try:
            return self._original_forward(*args, **kwargs)
        finally:
            self._offload()

    self.module.forward = forward
```

为什么用 `finally`：确保 forward 抛错时也恢复 offload 状态，避免后续请求看到半 onload 状态。

### 3.3 第三段：预取 offloader

`AscendPrefetchOffloader` 是默认路径，目标是减少重复分配和等待。

新增状态：

```python
class _ParamState:
    ...
    static_tensor: torch.Tensor | None = None
```

新增 buffer pool：

```python
class _StaticBufferPool:
    def __init__(self, *, slot_capacity: int, device: torch.device):
        self.slot_capacity = slot_capacity
        self.device = device
        self.total_bytes = 0
        self._buffers = {}
```

预取生命周期：

1. `post_init()` 创建 NPU copy stream 和 static buffer pool
2. 每个 offloaded module 绑定一个 `slot_idx`
3. 参数 host copy 保存在 `cpu_tensor`
4. 参数运行时指向 `static_tensor`
5. `start_prefetch()` 在 copy stream 中把 CPU tensor copy 到 static tensor
6. `wait_prefetch()` 在当前 stream 等待 copy done event
7. 当前 layer forward 后触发后续 layer 的 prefetch

关键代码形态：

```python
fork_event = torch.npu.Event()
torch.npu.current_stream().record_event(fork_event)
copy_stream.wait_event(fork_event)
with torch.npu.stream(copy_stream):
    state.static_tensor.copy_(state.cpu_tensor, non_blocking=True)
self.copy_done_event = torch.npu.Event()
self.copy_done_event.record(copy_stream)
```

### 3.4 参数过滤

`offload_params` 用于限制只 offload 某些参数名片段：

```python
def _matches_param(self, name: str) -> bool:
    if not self.offload_params:
        return True
    return any(f".{part}." in f".{name}." for part in self.offload_params)
```

设计要点：

- 空集合表示 offload 全部参数
- 使用点号包裹，减少子串误匹配
- sync 和 prefetch 使用同一规则

### 3.5 统计字段

offloader 应记录可观测状态，便于测试和日志：

| 字段 | sync | prefetch | 用途 |
|---|---|---|---|
| `selected_layer_indices` | yes | yes | 验证参数选择 |
| `module_offloaders` | yes | yes | 验证 wrap 数 |
| `total_offloaded_bytes` | yes | yes | 估算 HBM 节省 |
| `static_buffer_bytes` | no | yes | 估算静态 buffer 成本 |
| `copy_stream` | no | yes | 判断是否进入 NPU 预取路径 |

### 3.6 实现 Prompt 模板

```text
任务：实现 Ascend 权重 offloader。

请按三段式提交：
1. selection.py：纯函数，只做参数语义和边界校验
2. sync.py：同步 fallback，forward 前 onload，finally 中 offload
3. prefetch.py：static buffer + torch.npu.Stream/Event 预取

硬性要求：
- 默认参数必须 Noop
- sync 和 prefetch 必须复用同一个 select_offload_layers()
- 每个 offloader 必须暴露 selected_layer_indices 和 total_offloaded_bytes
- prefetch_step <= 0 必须报错
- 不要把参数逻辑写进 benchmark 脚本
```

### 3.7 失败案例：异步预取没有 fallback 会难以定位

**问题**：如果只实现 `prefetch`，一旦输出异常或性能异常，无法判断是 layer 选择错、CPU tensor 生命周期错、static buffer 错，还是 NPU stream/event 错。

**修正**：实现 `sync` 后端作为保守路径，并通过环境变量切换：

```text
ASCEND_OFFLOAD_IMPL=sync
ASCEND_OFFLOAD_IMPL=prefetch
```

**教训**：硬件异步优化要有同步参照组。AI 写异步代码时尤其需要一个慢但可解释的实现做对照。

---

## §4 Runner 集成：单一 factory 接入法

### 4.1 Factory 职责

`create_ascend_weight_offloader(offload_config)` 负责把配置对象转换成具体 offloader。

它应该集中处理：

1. 从 `offload_config.prefetch` 读取 `offload_group_size` / `offload_num_in_group`
2. 默认参数返回 `NoopOffloader`
3. `num_in_group > group_size` 报错
4. 读取 `ASCEND_OFFLOAD_IMPL` / `VLLM_ASCEND_OFFLOAD_IMPL`
5. 根据环境变量选择 `AscendSyncOffloader` 或 `AscendPrefetchOffloader`
6. 非法实现名报错

不要把这些逻辑散落到 runner 多个方法里。

### 4.2 推荐 factory 伪代码

```python
def create_ascend_weight_offloader(offload_config):
    prefetch_config = offload_config.prefetch
    group_size = prefetch_config.offload_group_size
    num_in_group = prefetch_config.offload_num_in_group

    if group_size <= 0 or num_in_group <= 0:
        return NoopOffloader()
    if num_in_group > group_size:
        raise ValueError(...)

    impl = os.getenv(
        "ASCEND_OFFLOAD_IMPL",
        os.getenv("VLLM_ASCEND_OFFLOAD_IMPL", "prefetch"),
    )
    if impl == "sync":
        return AscendSyncOffloader(...)
    if impl == "prefetch":
        return AscendPrefetchOffloader(...)
    raise ValueError(...)
```

### 4.3 集成位置

在 runner 初始化或模型加载前设置：

```python
set_offloader(create_ascend_weight_offloader(self.offload_config))
```

放置原则：

- 必须早于模型 layer wrapping
- 必须晚于 `offload_config` 可用
- 不要在每次 forward 中重复设置
- 不要在 benchmark 脚本里手工调用

### 4.4 日志与可观测性

模型加载或 `post_init()` 后记录：

```text
selected_layers=(...)
wrapped=<count>
saved=<GB>
static_buffer=<GB>
group_size=<N>
num_in_group=<M>
```

这些日志不是装饰，它们用于回答：

1. 参数是否真的生效？
2. 选中了哪些 layer？
3. 实际 wrap 了多少 module？
4. HBM 节省是否随参数变化？
5. prefetch 的 static buffer 成本是多少？

### 4.5 Runner 集成 Prompt 模板

```text
任务：把 Ascend offloader 接入 model_runner_v1.py。

要求：
1. 只新增一个 create_ascend_weight_offloader(factory)
2. 在模型加载前调用 set_offloader()
3. 默认参数返回 NoopOffloader
4. M>N 和未知 ASCEND_OFFLOAD_IMPL 报 ValueError
5. 日志打印 selected_layers、wrapped、saved GB、static buffer GB
6. 不要修改具体 model class

请先指出插入位置，再给 patch。
```

### 4.6 失败案例：环境变量名不兼容

**问题**：如果只支持一个环境变量名，用户在不同脚本或历史版本里可能设置另一个名字，导致以为切到了 sync，实际仍跑 prefetch。

**修正**：factory 兼容两种名字：

```python
os.getenv("ASCEND_OFFLOAD_IMPL", os.getenv("VLLM_ASCEND_OFFLOAD_IMPL", "prefetch"))
```

**教训**：调试开关要在 factory 中集中处理，并在测试中覆盖 sync、prefetch、非法值。

---

## §5 测试与审查：参数级测试护栏

### 5.1 测试矩阵

| 测试文件 | 必须覆盖 |
|---|---|
| `test_selection.py` | Noop、负数、M>N、末尾 N 层选择 |
| `test_offload_group_size_param.py` | 单独改变 group size 时 selected layers 变化 |
| `test_offload_num_in_group_param.py` | 单独改变 num in group 时 selected layers 变化 |
| `test_sync.py` | CPU capture、forward hook、finally offload、统计字节 |
| `test_prefetch.py` | static buffer pool、prefetch_step 校验、统计字段 |
| `test_runner_integration.py` | factory 默认 Noop、sync/prefetch/非法 env、M>N 报错 |

### 5.2 每个参数的独立测试写法

`offload_group_size` 测试关注“分组变化”：

```text
num_layers=8, num_in_group=1
group_size=4 -> (3, 7)
group_size=2 -> (1, 3, 5, 7)
```

`offload_num_in_group` 测试关注“每组数量变化”：

```text
num_layers=8, group_size=4
num_in_group=1 -> (3, 7)
num_in_group=2 -> (2, 3, 6, 7)
```

不要只断言 `len(layer_indices)`，必须断言具体 tuple。

### 5.3 AI 代码审查清单

每次 AI 修改 offloader 代码后，按此清单审查：

- [ ] 默认参数是否返回 `NoopOffloader`
- [ ] `select_offload_layers()` 是否是 sync/prefetch 的唯一选择来源
- [ ] `num_in_group > group_size` 是否显式报错
- [ ] 选择方向是否仍为“每组末尾 N 层”
- [ ] sync forward 是否用 `finally` offload
- [ ] prefetch 是否在等待 copy event 后再 forward
- [ ] `prefetch_step <= 0` 是否报错
- [ ] CPU tensor 是否 `.detach().cpu().contiguous()`
- [ ] static buffer 是否按 shape/stride/dtype 区分
- [ ] runner 是否只在模型加载前设置 offloader
- [ ] 测试是否覆盖 sync、prefetch、非法 env、默认 Noop

### 5.4 测试 Prompt 模板

```text
任务：为 Ascend offload 参数实现测试。

要求：
1. test_selection.py 覆盖全部边界
2. test_offload_group_size_param.py 只测试 group_size 的独立影响
3. test_offload_num_in_group_param.py 只测试 num_in_group 的独立影响
4. test_sync.py 验证 forward 前后参数设备/状态变化
5. test_prefetch.py mock 或隔离 NPU 相关路径，验证 static buffer 和统计字段
6. test_runner_integration.py 覆盖 Noop、sync、prefetch、非法 env、M>N

不要依赖真实模型完成参数语义测试。
```

### 5.5 失败案例：只测 offload 数量，不测具体 layer

**问题**：`group_size=4,num_in_group=1` 和某些错误实现都可能选出 2 个 layer。如果测试只断言数量为 2，无法发现“前 N 层”和“末尾 N 层”的语义差异。

**修正**：断言具体 tuple：

```python
assert selection.layer_indices == (3, 7)
```

**教训**：参数语义测试要断言结构，不要只断言数量。

---

## §6 推荐 Prompt 模板汇总

### 6.1 参数调研

```text
请详细调研 offload_group_size / offload_num_in_group。
不要写代码，先输出：
- 参数来源和默认值
- layer 选择公式
- 8 个输入输出样例
- 非法参数处理策略
- 可无设备测试的纯函数 API
- 与 vLLM BaseOffloader 的关系
```

### 6.2 上游链路

```text
请追踪 vLLM offloader 链路：
OffloadConfig -> set_offloader -> get_offloader -> module wrapping。
再找 vLLM Ascend 中最小接入点。
输出文件路径、函数名和为什么不修改 model class。
```

### 6.3 实现

```text
请按 Noop -> sync -> prefetch 三段式实现。
先给文件级设计，再给 patch。
硬性要求：
- selection.py 不 import torch
- sync forward 用 try/finally
- prefetch 使用 static buffer pool
- runner 用单一 factory
- 默认 Noop 不改变行为
```

### 6.4 审查

```text
请 review 这次 Ascend offload patch。
优先找：
1. 参数语义与测试不一致
2. 默认路径回归
3. M>N 未报错
4. sync 没有 finally offload
5. prefetch event/stream 生命周期错误
6. static buffer key 不足导致 shape/dtype 混用
7. runner 设置 offloader 的时机错误
```

---

## §7 AI 协作的反模式

| 反模式 | 风险 | 正确做法 |
|---|---|---|
| 先写 NPU stream 预取，再补参数语义 | bug 难定位 | 先写 `selection.py` 纯函数 |
| 默认参数也创建 offloader wrapper | 破坏兼容性 | 默认返回 `NoopOffloader` |
| 只实现 prefetch | 没有正确性参照 | 同时实现 `sync` fallback |
| layer 选择只测数量 | 前 N / 后 N 语义错误无法暴露 | 断言具体 layer tuple |
| 在 benchmark 脚本里模拟参数 | 不是后端能力 | 接入 runner + vLLM offloader |
| static buffer key 只按 name | shape/dtype 复用风险 | key 包含 name、shape、stride、dtype |
| forward hook 不用 finally | 异常后状态污染 | sync offload 放入 `finally` |
| 环境变量散落多处 | 行为不可预测 | factory 统一解析 |

---

## §8 总结：本次开发的三条原则

1. **参数语义是根，offloader 是枝。**  
   `offload_group_size` / `offload_num_in_group` 的选择规则必须先由纯函数和单测锁定，否则 sync 和 prefetch 都可能在错误语义上“正确运行”。

2. **同步路径是异步优化的基准线。**  
   `AscendSyncOffloader` 不是多余代码，而是定位 `AscendPrefetchOffloader` 问题的参照物。硬件异步优化没有同步参照，调试会变成猜测。

3. **Runner 集成要窄，状态观测要足。**  
   只在 `model_runner_v1.py` 增加 factory 和 `set_offloader()`，保持改动面小；同时记录 selected layers、wrapped count、saved GB 和 static buffer GB，让参数是否生效可以被测试和日志共同证明。

这份 skill 的目标是让后续 AI agent 能复现“先调研参数语义，再实现 Ascend offloader，再用测试护栏防止语义漂移”的开发路径。
