forked from vllm-ascend/ccf-vllm-ascend
Compare commits
53 Commits
master
...
docs/final
| Author | SHA1 | Date |
|---|---|---|
|
|
fabdf80e65 | |
|
|
30f29e3f10 | |
|
|
70b00b4c65 | |
|
|
10a8a27df0 | |
|
|
b425d3b760 | |
|
|
76d7ed1c94 | |
|
|
49065ee793 | |
|
|
851feb5fb2 | |
|
|
7ebcea1705 | |
|
|
cdb8299fe0 | |
|
|
c21e6eab1b | |
|
|
5a1911b5f7 | |
|
|
ec673f8d80 | |
|
|
c05189e025 | |
|
|
6688ab4aae | |
|
|
99241aa2a0 | |
|
|
eddc42d4ef | |
|
|
7e784f6508 | |
|
|
d87dfc3e88 | |
|
|
c63b6a6ffc | |
|
|
0bc69a60ab | |
|
|
5db06be370 | |
|
|
b2a5e98e28 | |
|
|
9a1face5fa | |
|
|
a19ebc2b14 | |
|
|
70530fd030 | |
|
|
7430606add | |
|
|
d8d8eed6cc | |
|
|
27aadd1413 | |
|
|
a41b39990f | |
|
|
680e86e24e | |
|
|
14f92d7c18 | |
|
|
19dcd61c53 | |
|
|
181b1a7ab4 | |
|
|
d4b69fd1e5 | |
|
|
0b12e7edc8 | |
|
|
c85c98f6f3 | |
|
|
801a1547e6 | |
|
|
26802027a0 | |
|
|
e2cd297c5e | |
|
|
fd174fd671 | |
|
|
bc33c335f1 | |
|
|
c608f9223f | |
|
|
79c366f184 | |
|
|
8e1f6d900e | |
|
|
460c56bafa | |
|
|
14a5dbfa3c | |
|
|
d7962b8a54 | |
|
|
2d663a88ce | |
|
|
8869b07e40 | |
|
|
f0ab80524e | |
|
|
543eac9ac3 | |
|
|
dbf5ad1546 |
|
|
@ -0,0 +1 @@
|
|||
*.sh text eol=lf
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.worktrees/
|
||||
|
|
@ -0,0 +1,741 @@
|
|||
---
|
||||
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,再用测试护栏防止语义漂移”的开发路径。
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
# Submission Manifest
|
||||
|
||||
Target: vLLM Ascend v0.18.0
|
||||
|
||||
## Implementation
|
||||
|
||||
- `vllm_ascend/worker/model_runner_v1.py`
|
||||
- `vllm_ascend/model_executor/offloader/prefetch.py`
|
||||
- `vllm_ascend/model_executor/offloader/selection.py`
|
||||
- `vllm_ascend/model_executor/offloader/sync.py`
|
||||
- `vllm_ascend/model_executor/offloader/__init__.py`
|
||||
|
||||
## Tests
|
||||
|
||||
- `tests/model_executor/offloader/test_offload_group_size_param.py`
|
||||
- `tests/model_executor/offloader/test_offload_num_in_group_param.py`
|
||||
- `tests/model_executor/offloader/test_prefetch.py`
|
||||
- `tests/model_executor/offloader/test_runner_integration.py`
|
||||
- `tests/model_executor/offloader/test_selection.py`
|
||||
- `tests/model_executor/offloader/test_sync.py`
|
||||
- `tests/model_executor/offloader/test_acceptance_scripts.py`
|
||||
- `tests/model_executor/offloader/test_accuracy_repro.py`
|
||||
|
||||
## Reproduction
|
||||
|
||||
- `scripts/install_v018_runtime_compat.sh`
|
||||
- `scripts/check_npu_health.sh`
|
||||
- `scripts/bench_one.py`
|
||||
- `scripts/bench_param_matrix.py`
|
||||
- `scripts/run_benchmark_all.sh`
|
||||
- `scripts/accuracy_gsm8k_gpqa.py`
|
||||
- `scripts/verify_accuracy_matrix.py`
|
||||
- `scripts/run_accuracy_gsm8k_gpqa.sh`
|
||||
- `scripts/run_reproduce_all.sh`
|
||||
|
||||
## Documentation
|
||||
|
||||
- `docs/v018-deployment-and-reproduction.md`
|
||||
- `docs/v018-test-evidence.md`
|
||||
- `docs/offload-group-size/technical-plan.md`
|
||||
- `docs/offload-num-in-group/technical-plan.md`
|
||||
|
||||
Historical scripts and reports are not canonical v0.18.0 evidence. Use only artifacts generated by `run_reproduce_all.sh` for submission metrics.
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
|
||||
# vLLM Ascend offload 参数性能测试报告
|
||||
|
||||
> 测试日期: 2026-07-05
|
||||
> 测试模型: `Qwen2.5-1.5B-Instruct`
|
||||
> 日志目录: `/data/vllm-ascend-smoke/logs/param-1p5b-20260705-114126`
|
||||
|
||||
## 一、测试概述
|
||||
|
||||
本报告对 `offload_group_size` 与 `offload_num_in_group` 的不同组合进行量化评估,比较 baseline 与各参数组合的吞吐和估算 HBM 节省。
|
||||
|
||||
## 二、测试方法
|
||||
|
||||
运行脚本:
|
||||
|
||||
```bash
|
||||
/data/vllm-ascend-smoke/run_benchmark_all.sh
|
||||
```
|
||||
|
||||
每个 case 加载同一 1.5B 模型,执行固定 prompt 集合,并从日志中提取:
|
||||
|
||||
- `output_tokens_per_second`
|
||||
- offload layer 数量
|
||||
- 估算节省 HBM
|
||||
|
||||
## 三、结果
|
||||
|
||||
| case | group_size | num_in_group | offload layers | saved GB | output tok/s |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| baseline_0_1 | 0 | 1 | 0 | 0.0000 | 38.8511 |
|
||||
| group_8_num_1 | 8 | 1 | 3 | 0.2615 | 41.7467 |
|
||||
| group_4_num_1 | 4 | 1 | 7 | 0.6102 | 24.5113 |
|
||||
| group_2_num_1 | 2 | 1 | 14 | 1.2203 | 14.5874 |
|
||||
| group_4_num_2 | 4 | 2 | 14 | 1.2203 | 14.9576 |
|
||||
|
||||
## 四、分析
|
||||
|
||||
1. `group_8_num_1` 只 offload 少量层,吞吐与 baseline 接近,并获得约 0.26 GB HBM 释放。
|
||||
2. `group_4_num_1` 释放约 0.61 GB HBM,但短请求吞吐下降明显。
|
||||
3. `group_2_num_1` 与 `group_4_num_2` offload 层数相同,节省 HBM 接近,吞吐也接近,验证两个参数的选择语义一致。
|
||||
4. 该参数适合 HBM 紧张、需要换取可加载模型规模或并发空间的场景;不适合作为纯吞吐优化开关。
|
||||
|
||||
## 五、限制
|
||||
|
||||
当前 benchmark 只覆盖单卡、短 prompt、Python LLM API。后续 PR 可补充 server streaming TTFT、长上下文和多并发压测。
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
|
||||
# vLLM Ascend 参数适配专项赛 — 最终结项材料
|
||||
|
||||
> 赛道: CCF 开源创新大赛 vLLM Ascend 参数适配专项赛
|
||||
> 认领参数: `offload_group_size` + `offload_num_in_group`
|
||||
> 测试日期: 2026-07-05
|
||||
> 参考提交材料结构: `Aiminer/ccf-vllm-ascend:feature/dbo-re-ascend-adapt`
|
||||
|
||||
---
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
本次工作在 vLLM Ascend 后端适配权重 offload 参数,使用户可按 decoder layer group 控制部分层权重驻留在 host/static buffer,并在需要时换入 NPU 执行。
|
||||
|
||||
## 二、参数理解与技术难点
|
||||
|
||||
| 参数 | 含义 | 难点 |
|
||||
|---|---|---|
|
||||
| `offload_group_size` | 每多少层组成一个 offload group | 需要稳定映射 layer name 到 layer id |
|
||||
| `offload_num_in_group` | 每个 group 中 offload 的层数 | 需要校验 `num <= group_size`,并保证默认行为不变 |
|
||||
|
||||
技术难点包括:
|
||||
|
||||
1. Ascend OOT 插件需要在模型加载前注入 vLLM offloader。
|
||||
2. offload wrap 与真实模型 layer 命名存在耦合,必须可测试。
|
||||
3. NPU 权重换入换出需要同时提供保守同步路径与可扩展预取路径。
|
||||
4. 参数的收益不是单纯吞吐提升,而是 HBM 节省与吞吐之间的 trade-off,必须通过 benchmark 呈现。
|
||||
|
||||
## 三、技术方案
|
||||
|
||||
新增 `vllm_ascend/model_executor/offloader/` 模块:
|
||||
|
||||
- `selection.py`: 实现 group 参数选择规则。
|
||||
- `sync.py`: 实现同步权重 offload。
|
||||
- `prefetch.py`: 实现静态 buffer / 预取 offload。
|
||||
- `__init__.py`: 暴露公共接口。
|
||||
|
||||
修改 `worker/model_runner_v1.py`:
|
||||
|
||||
- 新增 `create_ascend_weight_offloader(offload_config)`。
|
||||
- 在 runner 初始化阶段调用 `set_offloader()`。
|
||||
- 增加参数校验、实现选择和日志统计。
|
||||
|
||||
## 四、实际交付
|
||||
|
||||
| 类型 | 文件 |
|
||||
|---|---|
|
||||
| 源码 | `vllm_ascend/model_executor/offloader/*.py` |
|
||||
| 集成 | `vllm_ascend/worker/model_runner_v1.py` |
|
||||
| 单测 | `tests/model_executor/offloader/*.py`, `tests/model_executor/offloader/offload_group_size/*.py`, `tests/model_executor/offloader/offload_num_in_group/*.py` |
|
||||
| 冒烟脚本 | `/data/vllm-ascend-smoke/run_smoke*.sh` |
|
||||
| benchmark | `/data/vllm-ascend-smoke/run_benchmark_all.sh` |
|
||||
| 稳定性 | `/data/vllm-ascend-smoke/run_stability_100.sh` |
|
||||
| 文档 | `docs/offload-group-size/*.md`, `docs/offload-num-in-group/*.md`, `docs/benchmark/*.md` |
|
||||
|
||||
## 五、测试结论
|
||||
|
||||
单测:
|
||||
|
||||
```text
|
||||
35 passed, 3 warnings
|
||||
```
|
||||
|
||||
真实 Ascend 冒烟:
|
||||
|
||||
- baseline: `SMOKE_OK`
|
||||
- prefetch offload: `SMOKE_OK_OFFLOAD`
|
||||
- sync offload: `SMOKE_OK_OFFLOAD`
|
||||
- invalid `offload_num_in_group > offload_group_size`: `SMOKE_INVALID_OK`
|
||||
|
||||
1.5B benchmark 表明:
|
||||
|
||||
- 少量 offload 可释放 HBM 且吞吐接近 baseline。
|
||||
- offload 层数增加后 HBM 节省增加,但吞吐下降。
|
||||
- `offload_group_size` 与 `offload_num_in_group` 的 layer 选择语义被 benchmark 和单测共同验证。
|
||||
|
||||
## 六、AI 工具使用经验
|
||||
|
||||
本次开发采用 AI 协助完成环境排查、代码实现、测试补齐和文档整理。关键经验:
|
||||
|
||||
1. 先用最小模型跑通冒烟,再扩大到 1.5B 模型做参数矩阵。
|
||||
2. 每个参数都需要单独测试,不只依赖端到端推理。
|
||||
3. 对性能参数,PR 材料必须同时说明收益和代价,避免只报告通过状态。
|
||||
4. 对参考 PR/分支,优先复用其交付结构,而不是照抄具体实现。
|
||||
|
||||
## 七、限制与后续工作
|
||||
|
||||
- 当前验证聚焦单卡 eager 推理。
|
||||
- TTFT 使用 `LLM.generate(max_tokens=1)` proxy,后续可补 server streaming TTFT。
|
||||
- 后续可继续验证长上下文、多并发、多卡和 ACL graph 场景。
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
# 当前实现与最新版 vLLM Ascend 性能对比
|
||||
|
||||
## 1. 对比目标
|
||||
|
||||
- 当前分支:`feature/offload-group-num-v018-optimized`
|
||||
- 当前实现提交:`0b12e7e`
|
||||
- 最新版基线:vLLM Ascend `v0.23.0rc1`
|
||||
- 最新版源码提交:`f4a08bddd0cc65a0bd8c3d377b158ae5ca7527db`
|
||||
- vLLM:`0.23.0`
|
||||
- 测试日期:2026-07-30
|
||||
|
||||
本报告同时回答两个问题:
|
||||
|
||||
1. 当前实现的两个参赛参数相对最新版无卸载基线有多少吞吐代价。
|
||||
2. 最新版主仓已实现相同参数时,当前实现相对主仓同参数实现是否有可量化性能提升。
|
||||
|
||||
这里使用两个基线概念:
|
||||
|
||||
- **最新版无卸载基线:** 官方 `v0.23.0rc1`,`offload_group_size=0`、
|
||||
`offload_num_in_group=1`,不设置 `offload_backend`。
|
||||
- **最新版同参数实现:** 官方 `v0.23.0rc1` 的
|
||||
`NPUPrefetchOffloader`,分别测试 `(8,1)` 与 `(8,2)`。
|
||||
|
||||
## 2. 适配与公平性
|
||||
|
||||
当前分支基于 vLLM Ascend v0.18.0 开发,最新版 runner 的公开类名已经变为
|
||||
`NPUPrefetchOffloader`。为避免把跨版本 runner API 差异混入 offloader 性能,
|
||||
本次测试:
|
||||
|
||||
1. 统一使用最新版 v0.23.0rc1 runner、vLLM、CANN、模型和硬件。
|
||||
2. 当前实现 case 只覆盖本分支
|
||||
`vllm_ascend/model_executor/offloader/`。
|
||||
3. 在临时 overlay 中增加
|
||||
`NPUPrefetchOffloader = AscendPrefetchOffloader` 类名兼容别名。
|
||||
4. 不修改当前实现的数据搬运、静态缓冲、事件同步或层选择逻辑。
|
||||
5. 每份当前实现日志必须出现
|
||||
`Offloader set to AscendPrefetchOffloader`;官方同参数日志必须出现
|
||||
`Offloader set to NPUPrefetchOffloader`。
|
||||
|
||||
该方式隔离比较了两套 offloader 实现,同时保持 runner 与底层软件栈一致。
|
||||
|
||||
## 3. 测试环境
|
||||
|
||||
| 项目 | 配置 |
|
||||
|---|---|
|
||||
| NPU | Ascend 910B2C,物理卡 6 / 逻辑卡 0 |
|
||||
| CANN | 9.0.1 |
|
||||
| Python | 3.12.13 |
|
||||
| vLLM | 0.23.0 |
|
||||
| vLLM Ascend | 0.23.0rc1 |
|
||||
| PyTorch | 2.10.0 |
|
||||
| torch-npu | 2.10.0.post2 |
|
||||
| triton-ascend | 3.2.1 |
|
||||
| 模型 | Qwen2.5-1.5B-Instruct |
|
||||
| dtype | FP16 |
|
||||
| 执行模式 | eager |
|
||||
|
||||
最新版 wheel SHA-256:
|
||||
|
||||
```text
|
||||
2f02c7a31e6ed565084876aebb6cca0bc8e60d8988fdacb3d0f69f4caa8138c8
|
||||
```
|
||||
|
||||
当前实现适配包 SHA-256:
|
||||
|
||||
```text
|
||||
d90513b1555362d57981d317ca956391db364cf5bb15b2362c8482a6713177d1
|
||||
```
|
||||
|
||||
## 4. 测试协议
|
||||
|
||||
| 参数 | 值 |
|
||||
|---|---:|
|
||||
| prompt 数 | 8 |
|
||||
| 每 prompt 输入 token | 128 |
|
||||
| 每 prompt 最大输出 token | 64 |
|
||||
| `max_model_len` | 512 |
|
||||
| `gpu_memory_utilization` | 0.5 |
|
||||
| 每进程预热轮数 | 1 |
|
||||
| 每进程计时轮数 | 3 |
|
||||
| 每配置独立进程数 | 3 |
|
||||
| 每进程计时输出 token | 1536 |
|
||||
| seed | 0 |
|
||||
|
||||
五个配置共启动 15 个独立进程。官方与当前实现采用交错顺序运行,并在进程之间
|
||||
等待 3 秒。主指标是每个独立进程整体输出吞吐的三次中位数。
|
||||
|
||||
## 5. 正式结果
|
||||
|
||||
### 5.1 原始吞吐与中位数
|
||||
|
||||
| 配置 | 三次吞吐率 (tok/s) | 中位数 (tok/s) | 相对无卸载基线 |
|
||||
|---|---:|---:|---:|
|
||||
| 最新版无卸载基线 | 571.023 / 597.419 / 572.033 | **572.033** | 0.000% |
|
||||
| 最新版官方实现 `(8,1)` | 572.020 / 548.081 / 573.158 | **572.020** | -0.002% |
|
||||
| 当前实现 `(8,1)` | 520.096 / 570.207 / 573.226 | **570.207** | **-0.319%** |
|
||||
| 最新版官方实现 `(8,2)` | 381.222 / 379.922 / 381.196 | **381.196** | -33.361% |
|
||||
| 当前实现 `(8,2)` | 380.943 / 381.134 / 381.016 | **381.016** | **-33.393%** |
|
||||
|
||||
### 5.2 当前实现与最新版同参数实现
|
||||
|
||||
| 参数 | 当前实现中位数 | 最新版官方中位数 | 当前实现差值 |
|
||||
|---|---:|---:|---:|
|
||||
| `(8,1)` | 570.207 tok/s | 572.020 tok/s | -1.813 tok/s(**-0.317%**) |
|
||||
| `(8,2)` | 381.016 tok/s | 381.196 tok/s | -0.179 tok/s(**-0.047%**) |
|
||||
|
||||
### 5.3 对离群运行的敏感性检查
|
||||
|
||||
`(8,1)` 的当前实现和官方实现分别出现一次较慢进程。将每个配置的 9 个计时轮次
|
||||
直接合并后取轮次吞吐中位数,结果为:
|
||||
|
||||
| 配置 | 9 轮吞吐中位数 (tok/s) | 相对同参数官方实现 |
|
||||
|---|---:|---:|
|
||||
| 最新版无卸载基线 | 572.054 | - |
|
||||
| 最新版官方实现 `(8,1)` | 571.460 | - |
|
||||
| 当前实现 `(8,1)` | 570.891 | -0.099% |
|
||||
| 最新版官方实现 `(8,2)` | 381.250 | - |
|
||||
| 当前实现 `(8,2)` | 380.995 | -0.067% |
|
||||
|
||||
该敏感性检查不替代预先约定的进程级中位数,只用于确认结论不由单个慢进程决定。
|
||||
|
||||
## 6. 结论
|
||||
|
||||
1. 当前实现 `(8,1)` 的吞吐与最新版无卸载基线基本持平,主指标下降
|
||||
0.319%;在 9 轮敏感性检查中下降 0.203%。
|
||||
2. 当前实现 `(8,2)` 相对无卸载基线下降 33.393%,说明每 8 层卸载 2 层的
|
||||
CPU-NPU 传输开销明显。
|
||||
3. 两个参数下,当前实现与最新版官方实现的主指标差异分别为 -0.317% 和
|
||||
-0.047%,轮次中位数差异分别为 -0.099% 和 -0.067%。
|
||||
4. 结合运行间波动,本次负载下两套 offloader 的吞吐可视为同一水平。
|
||||
5. **本轮性能测试没有证明当前实现相对最新版主仓实现有可量化性能提升。**
|
||||
因此,如果按“主仓已有实现时必须在性能或精度至少一个维度提升”的附加要求
|
||||
评审,不能把本轮结果表述为性能提升证据。
|
||||
|
||||
`npu_hbm_after_mib` 受固定 `gpu_memory_utilization=0.5` 下 KV cache 自动填充影响,
|
||||
不能直接作为权重卸载节省量。本报告只据此比较性能,不把进程结束前的 HBM
|
||||
占用解释为容量收益。
|
||||
|
||||
## 7. 完整性校验
|
||||
|
||||
- 15/15 个正式运行成功,五个配置各 3 次。
|
||||
- 6/6 个当前实现日志加载 `AscendPrefetchOffloader`。
|
||||
- 6/6 个最新版官方日志加载 `NPUPrefetchOffloader`。
|
||||
- 3/3 个无卸载基线日志未加载 prefetch offloader。
|
||||
- 0 个 Traceback。
|
||||
- 每份日志恰好包含 1 条 `BENCH_RESULT`。
|
||||
- 当前实现的 v0.18 原生 offloader 测试套件为 `88 passed`。
|
||||
- 本地归档内 22 个文件逐一通过 SHA-256 校验。
|
||||
|
||||
完整原始归档:
|
||||
|
||||
```text
|
||||
D:\PDSL\Ascend\current-vs-latest-performance-20260730-results.tar.gz
|
||||
```
|
||||
|
||||
归档 SHA-256:
|
||||
|
||||
```text
|
||||
0fcc0a759d7ac6a82ba272ca346cce95992759dbb5a77969d1535625d9b81b2f
|
||||
```
|
||||
|
||||
解压目录包含每次运行日志、原始 JSONL、CSV/JSON/Markdown 汇总、环境清单和
|
||||
校验清单:
|
||||
|
||||
```text
|
||||
D:\PDSL\Ascend\current-vs-latest-performance-20260730
|
||||
```
|
||||
|
||||
## 8. 复现
|
||||
|
||||
最新版环境与模型准备完成后,在当前分支执行:
|
||||
|
||||
```bash
|
||||
export CANN_ENV=/data/Ascend/cann/set_env.sh
|
||||
export ATB_ENV=/data/Ascend/nnal/atb/set_env.sh
|
||||
export VENV_ACTIVATE=/data/vllm-ascend-v0.23.0rc1/.venv-cann901/bin/activate
|
||||
export MODEL_PATH=/data/models/Qwen2.5-1.5B-Instruct
|
||||
export RESULTS_DIR=/data/current-vs-latest-performance-reproduce
|
||||
export NPU_INDEX=6
|
||||
export ASCEND_RT_VISIBLE_DEVICES=0
|
||||
|
||||
bash scripts/run_current_vs_latest_performance.sh
|
||||
```
|
||||
|
||||
脚本会构造临时当前实现 overlay、按本报告顺序运行 15 个进程、生成中位数汇总,
|
||||
并校验结果数量、实现类和 Traceback。
|
||||
|
||||
## 9. 版本来源
|
||||
|
||||
- [vLLM Ascend v0.23.0rc1 发布页](https://github.com/vllm-project/vllm-ascend/releases/tag/v0.23.0rc1)
|
||||
- [vLLM Ascend 官方安装文档](https://docs.vllm.ai/projects/ascend/en/latest/installation.html)
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
# vLLM Ascend v0.18.0 参数优化性能测试报告
|
||||
|
||||
## 1. 测试目标
|
||||
|
||||
本次测试面向当前实现的两个参数:
|
||||
|
||||
- `offload_group_size`:按多少层划分一个卸载组。
|
||||
- `offload_num_in_group`:每个组中卸载末尾多少层。
|
||||
|
||||
对 28 层模型,选层规则为:
|
||||
|
||||
```text
|
||||
layer_idx % offload_group_size
|
||||
>= offload_group_size - offload_num_in_group
|
||||
```
|
||||
|
||||
测试目标不是只寻找吞吐最高点,而是量化“卸载容量、可用 KV cache、吞吐”
|
||||
三者之间的取舍,并确定可用于默认配置的稳定工作区间。
|
||||
|
||||
## 2. 基线与环境
|
||||
|
||||
本报告中的基线是同一当前实现、同一 vLLM Ascend v0.18.0 运行环境下,
|
||||
关闭权重卸载的 `(offload_group_size=0, offload_num_in_group=1)`。它不是
|
||||
最新版 vLLM Ascend 的官方实现基线。
|
||||
|
||||
| 项目 | 配置 |
|
||||
|---|---|
|
||||
| 测试日期 | 2026-07-30 |
|
||||
| 当前分支 | `feature/offload-group-num-v018-optimized` |
|
||||
| 当前代码提交 | `0b12e7e` |
|
||||
| vLLM | `0.18.0+empty` |
|
||||
| vLLM Ascend 运行包 | `0.1.dev1+g72dc68973.d20260729` |
|
||||
| CANN | `9.0.1` |
|
||||
| NPU | Ascend 910B2C,物理卡 6 / 容器逻辑卡 0 |
|
||||
| 模型 | Qwen2.5-1.5B-Instruct |
|
||||
| 执行模式 | 单卡、eager、FP16 |
|
||||
|
||||
测试前后均确认 NPU 健康状态为 `OK`,测试结束后无残留推理进程。
|
||||
|
||||
## 3. 测试方法
|
||||
|
||||
### 3.1 主参数矩阵
|
||||
|
||||
- 11 组配置,共 43 个独立 Python 进程。
|
||||
- 基线及低密度 4 组配置各重复 5 次,其余配置各重复 3 次。
|
||||
- 每个进程预热 1 轮、测量 3 轮。
|
||||
- 每轮 8 个请求,输入 128 tokens,最多输出 64 tokens。
|
||||
- 每个进程输出 1,536 tokens。
|
||||
- `max_model_len=512`,`gpu_memory_utilization=0.5`。
|
||||
- 配置按轮换顺序执行,每次均重新加载模型,降低时间顺序和热状态偏差。
|
||||
- 主指标为“独立进程吞吐的中位数”。
|
||||
|
||||
### 3.2 长窗口确认
|
||||
|
||||
对基线、`(28,1)`、`(14,1)` 追加 9 个独立进程:
|
||||
|
||||
- 每组 3 个进程,使用 Latin-square 顺序。
|
||||
- 每个进程预热 1 轮、测量 10 轮。
|
||||
- 每个进程输出 5,120 tokens。
|
||||
- 每组汇集 30 个测量轮次,以 30 轮吞吐中位数确认低密度差异。
|
||||
|
||||
## 4. 主矩阵结果
|
||||
|
||||
| 参数 `(group_size,num)` | 卸载层 | 选中层 | 节省权重 (GB) | 可用 KV cache (GiB) | 进程吞吐中位数 (tok/s) | P50 延迟 (s) | 相对基线 | 吞吐保留率 | Pareto |
|
||||
|---|---:|---|---:|---:|---:|---:|---:|---:|---|
|
||||
| 基线 `(0,1)` | 0 | `()` | 0.0000 | 27.27 | 684.661 | 0.7512 | +0.000% | 100.000% | - |
|
||||
| `(28,1)` | 1 | `(27,)` | 0.0872 | 27.36 | 691.406 | 0.7394 | +0.985% | 100.985% | 是 |
|
||||
| `(14,1)` | 2 | `(13,27)` | 0.1743 | 27.45 | 688.071 | 0.7457 | +0.498% | 100.498% | 是 |
|
||||
| `(28,2)` | 2 | `(26,27)` | 0.1743 | 27.45 | 680.765 | 0.7494 | -0.569% | 99.431% | 否 |
|
||||
| `(8,1)` | 3 | `(7,15,23)` | 0.2615 | 27.53 | 663.416 | 0.7649 | -3.103% | 96.897% | 是 |
|
||||
| `(7,1)` | 4 | `(6,13,20,27)` | 0.3487 | 27.62 | 570.684 | 0.8971 | -16.647% | 83.353% | 是 |
|
||||
| `(14,2)` | 4 | `(12,13,26,27)` | 0.3487 | 27.62 | 565.651 | 0.9052 | -17.382% | 82.618% | 否 |
|
||||
| `(8,2)` | 6 | `(6,7,14,15,22,23)` | 0.5230 | 27.79 | 381.955 | 1.3405 | -44.213% | 55.787% | 是 |
|
||||
| `(4,1)` | 7 | `(3,7,11,15,19,23,27)` | 0.6102 | 27.88 | 328.878 | 1.5567 | -51.965% | 48.035% | 是 |
|
||||
| `(2,1)` | 14 | 14 个交替层 | 1.2203 | 28.49 | 164.826 | 3.1062 | -75.926% | 24.074% | 否 |
|
||||
| `(4,2)` | 14 | 14 个成对层 | 1.2203 | 28.49 | 164.928 | 3.1043 | -75.911% | 24.089% | 是 |
|
||||
|
||||
Pareto 判定同时以“节省权重更多”和“吞吐更高”为优。相同容量下被另一
|
||||
配置稳定超过的点不在前沿中。
|
||||
|
||||
## 5. 长窗口确认结果
|
||||
|
||||
| 配置 | 3 次进程吞吐 (tok/s) | 进程中位数 | 30 轮中位数 | 30 轮相对基线 | 30 轮标准差 |
|
||||
|---|---|---:|---:|---:|---:|
|
||||
| 基线 `(0,1)` | 694.946 / 666.142 / 651.593 | 666.142 | 686.133 | +0.000% | 40.712 |
|
||||
| `(28,1)` | 690.528 / 670.087 / 683.980 | 683.980 | 686.331 | +0.029% | 19.128 |
|
||||
| `(14,1)` | 697.923 / 683.060 / 675.299 | 683.060 | 688.708 | +0.375% | 16.872 |
|
||||
|
||||
`+0.029%` 和 `+0.375%` 均小于环境运行波动,不能据此声称参数带来了
|
||||
确定的吞吐加速。可以确认的是,`(28,1)` 与 `(14,1)` 在释放权重存储和
|
||||
增加 KV cache 的同时,典型吞吐与关闭卸载的基线相当。
|
||||
|
||||
## 6. 参数规律
|
||||
|
||||
1. **低密度区间是最佳工作区间。** 卸载 1 至 2 层时,吞吐未出现可测量
|
||||
的稳定损失;卸载 3 层时中位吞吐下降约 3.1%。
|
||||
2. **性能拐点位于 3 层之后。** 卸载 4 层时损失扩大到 16.6% 至 17.4%,
|
||||
P50 延迟由低密度区间的约 0.74 至 0.76 秒升至约 0.90 秒,继续增加
|
||||
层数后吞吐和延迟均呈非线性恶化。
|
||||
3. **层数相同时,分散单层通常优于相邻多层。** 两层配置中 `(14,1)` 比
|
||||
`(28,2)` 高 1.07%;四层配置中 `(7,1)` 比 `(14,2)` 高 0.89%。
|
||||
4. **高密度时总卸载层数成为主导因素。** 同为 14 层的 `(2,1)` 与
|
||||
`(4,2)` 吞吐仅相差约 0.06%。
|
||||
5. **容量收益近似线性,性能代价并非线性。** 每卸载一层约释放
|
||||
0.087 GB 权重存储并增加约 0.09 GiB KV cache,但超过拐点后传输等待
|
||||
会快速吞噬吞吐。
|
||||
|
||||
## 7. 推荐配置
|
||||
|
||||
**平衡默认值:`(14,1)`。**
|
||||
|
||||
- 卸载 2 个分散层,释放 0.1743 GB 权重存储。
|
||||
- 可用 KV cache 从 27.27 GiB 增至 27.45 GiB。
|
||||
- 长窗口吞吐与基线相当。
|
||||
- 相比同样卸载 2 层的 `(28,2)`,分散布局表现更好。
|
||||
|
||||
其他目标可按以下方式选择:
|
||||
|
||||
| 优化目标 | 推荐配置 | 说明 |
|
||||
|---|---|---|
|
||||
| 最保守容量扩展 | `(28,1)` | 释放 0.0872 GB,吞吐与基线相当 |
|
||||
| 平衡容量与吞吐 | `(14,1)` | 释放 0.1743 GB,推荐默认值 |
|
||||
| 接受约 3% 损失换容量 | `(8,1)` | 释放 0.2615 GB |
|
||||
| 吞吐敏感业务 | 不建议卸载 4 层及以上 | 已进入明显性能拐点 |
|
||||
|
||||
## 8. 完整性与限制
|
||||
|
||||
- 43 个主矩阵日志和 9 个确认日志均恰好包含一条 `BENCH_RESULT`。
|
||||
- 44 个启用卸载的日志均确认使用 `AscendPrefetchOffloader`。
|
||||
- 52 个日志均成功记录选层、包装层数、节省容量和 KV cache。
|
||||
- 52 个日志均无 Python traceback。
|
||||
- offloader 测试套件结果为 `92 passed in 11.68s`。
|
||||
- 当前 CANN 9.0.1 环境的 52 个日志均出现 custom ops 注册失败警告。
|
||||
所有配置处于相同降级状态,因此内部相对比较仍可用;绝对吞吐仅代表
|
||||
本次运行环境,不应与其他 CANN 环境的数值直接拼接比较。
|
||||
- 本次结论针对单卡 Qwen2.5-1.5B-Instruct、固定批量与固定输入输出长度。
|
||||
更大模型、长上下文或并发服务场景仍需按同一脚本追加验证。
|
||||
|
||||
## 9. 复现
|
||||
|
||||
完整矩阵:
|
||||
|
||||
```bash
|
||||
MODEL_PATH=/data/models/Qwen2.5-1.5B-Instruct \
|
||||
RESULTS_DIR=/data/v018-parameter-optimization-reproduce \
|
||||
NPU_INDEX=6 \
|
||||
ASCEND_RT_VISIBLE_DEVICES=0 \
|
||||
bash scripts/run_parameter_optimization_benchmark.sh
|
||||
```
|
||||
|
||||
对已有原始数据重新分析:
|
||||
|
||||
```bash
|
||||
python scripts/analyze_parameter_optimization.py \
|
||||
--results-dir /data/v018-parameter-optimization-reproduce
|
||||
```
|
||||
|
||||
相关文件:
|
||||
|
||||
- `scripts/run_parameter_optimization_benchmark.sh`
|
||||
- `scripts/analyze_parameter_optimization.py`
|
||||
- `scripts/bench_one.py`
|
||||
- `tests/model_executor/offloader/test_parameter_optimization_analysis.py`
|
||||
- 原始结果目录:`v018-parameter-optimization-20260730`
|
||||
- 原始结果归档:`v018-parameter-optimization-20260730-results.tar.gz`
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
# Qwen2.5-7B 权重卸载参数性能对比报告
|
||||
|
||||
## 结论
|
||||
|
||||
本实验未建立当前实现相对官方 latest 的可测量、稳定速度提升。在同一 v0.23.0rc1 运行时的 10 组正参数配对中,当前实现相对官方实现的进程吞吐中位数差为 **-0.086595% 至 +0.043851%**。低密度长窗口复测中,`(14,1)` 为 **+0.003119%**,`(28,1)` 为 **-0.299468%**。`(14,1)` 在短、长窗口虽均微正,但幅度小于或接近三进程范围所反映的运行变化;`(28,1)` 两阶段均未优于 latest。因此不能宣称当前实现优于官方主仓 latest。
|
||||
|
||||
参数选择应以容量和吞吐取舍为准:HBM 与 KV cache 充足时使用 baseline;需要约 **0.434 GiB** 额外容量时,`(28,1)` 是实际膝点并保留约 **85.77%** 吞吐;`(14,1)` 释放约 **0.868 GiB**,但只保留约 **44.68%** 吞吐,仅适合容量优先场景。本次 7B 运行未采集精度结果,不应从本文推导精度结论;项目既有 GSM8K/GPQA 精度证据属于独立工作。
|
||||
|
||||
## 实验对象与可追溯性
|
||||
|
||||
| 项目 | 固定值 |
|
||||
| --- | --- |
|
||||
| 模型 | `Qwen/Qwen2.5-7B-Instruct`,28 个 decoder layers,benchmark 转为 `float16` |
|
||||
| 模型本地身份 | `/data/models/Qwen2.5-7B-Instruct`;ModelScope revision `3fe289ea2828843e2d3f4f7449848eae7b1d8e7e`;模型 `SHA256SUMS` 文件 SHA-256 `1feefc067990d64ab8f584793ddbff5bd6cd725cc8a0b91a84d595ebcff04e14`,且清单内模型文件全部复核 |
|
||||
| 硬件 | Ascend 910B2C;逻辑卡 0,物理 NPU 6;`npu-smi` 26.0.rc1 |
|
||||
| 可见设备 | `ASCEND_RT_VISIBLE_DEVICES=0`,`NPU_INDEX=6`,`NPU_CHIP_INDEX=0` |
|
||||
| CANN | 9.0.1;权威文件 `/etc/Ascend/ascend_cann_install.info`,SHA-256 `ac255f290ec684bbf360e60afa64740e0f0422ea8e47627c96adda8a922d819e`;环境脚本 `/data/Ascend/cann/set_env.sh`,SHA-256 `787ff0ecb69abe17ed481baecb6e04340c4dc36c655dbedb2718bce13dd289a3` |
|
||||
| 官方 latest 比较目标 | [vLLM Ascend v0.23.0rc1(提交 `f4a08bd`)](https://github.com/vllm-project/vllm-ascend/releases/tag/v0.23.0rc1),本实验将其定义为 latest |
|
||||
| 官方 latest 源码身份 | clean checkout `/data/vllm-ascend-v0.23.0rc1-source-clean`,HEAD `f4a08bddd0cc65a0bd8c3d377b158ae5ca7527db`;完整 `vllm_ascend` tree SHA-256 `f1677c3dba91ded53030ee2c1538d85efc67c5762c5990814a8dc35b5980194f` |
|
||||
| 官方 latest 包身份 | `vllm-ascend==0.23.0rc1`、`vllm==0.23.0`、`torch==2.10.0+cpu`、`torch-npu==2.10.0.post2`、`triton-ascend==3.2.1`;wheel SHA-256 `2f02c7a31e6ed565084876aebb6cca0bc8e60d8988fdacb3d0f69f4caa8138c8`;wheel tree 与已安装 package tree SHA-256 均为 `58babd7d25b43a81d5f7a4d85f641d388f9b3d16cfe96bed9fb551b660c7ed8f` |
|
||||
| 当前实现的同运行时形态 | 从 official v0.23.0rc1 distribution 复制包后,仅覆盖 `vllm_ascend/model_executor/offloader`;其余运行时与官方 latest 相同 |
|
||||
| 项目部署 | commit `ec673f8`;root `/data/ccf-vllm-ascend-ec673f8-exact` |
|
||||
| 精确项目归档 | `/data/ccf-vllm-ascend-ec673f8-exact.tar.gz`;SHA-256 `15891f227dae91b671124e649f454aa42b8d85e1ccc97a67906469fe58ce1000` |
|
||||
| 项目实现身份 | offloader tree SHA-256 `0f864b7af056732895677053c1b215fae49b730634506a1a529ac4693e1f0f5f`;worker SHA-256 `c53a0369874faa92f5e6e109a035420f847656220b6c08949aee655d1f43133d`;runner SHA-256 `11c1dd36b3d8691f7bd4777817a11e0e2711068f4f453f9f5b1c1a6171f6cd2d` |
|
||||
| native v0.18 对照 | 源码 commit `72dc68973bd7e6ceef9a122de316a7ee9c9a84aa`;固定 root `/data/vllm-ascend-v0.18.0-hardened-v6-source`;完整 `vllm_ascend` tree SHA-256 `1c4ae545ce311eb2282f291f1cc315a541754d32eeddc87d82fdb41fdaae6091`;运行包版本为 `vllm==0.18.0+empty`、`torch==2.9.0+cpu`、`torch-npu==2.9.0.post2`、`triton-ascend==3.2.1` |
|
||||
|
||||
本报告只使用 hardened-v6 的原始不可变证据,根目录为 `D:\PDSL\Ascend\qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8-pre-report-extracted\qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8`。环境、包、源码、模型和部署身份记录于 `environment-manifest.json`、`latest-installed-package-sha256s.txt`、`latest-wheel-package-sha256s.txt`、`latest-source-tree-sha256s.txt`、`native-v018-source-tree-sha256s.txt`、`model-sha256s.txt`、`project-offloader-sha256s.txt`、`project-worker-sha256.txt` 和 `project-runner-sha256.txt`。
|
||||
|
||||
## 负载与证据规模
|
||||
|
||||
每个进程固定使用 8 个 prompts、128 输入 tokens、64 最大输出 tokens、`max_model_len=512`、`gpu_memory_utilization=0.5`、seed 0。短窗口每进程 1 个 warmup round 和 3 个 measured rounds;长窗口每进程 1 个 warmup round 和 10 个 measured rounds。各 case 独立加载模型,按 3 次重复的进程级吞吐中位数统计。
|
||||
|
||||
| 阶段 | 记录/日志/进程 | case 覆盖 |
|
||||
| --- | ---: | --- |
|
||||
| 同运行时 | 63 | baseline 加 10 个正参数对,每个 label 3 次 |
|
||||
| 长窗口 | 15 | baseline、`(28,1)`、`(14,1)` 的 current/latest,每个 label 3 次 |
|
||||
| native v0.18 | 12 | baseline、`(28,1)`、`(14,1)`、`(8,1)`,每个 label 3 次 |
|
||||
| 合计 | **90** | **90 条记录、90 份独立日志、90 份运行时证据** |
|
||||
|
||||
`validation.json` 为 `valid: true`、`model_files_verified: true`,`.partial.log` 文件计数为 0,且没有 validation error。90 份过程日志中 `Traceback` 计数为 0。原始记录为 `same-runtime-runs.jsonl`、`long-window-runs.jsonl`、`native-v018-runs.jsonl`,逐进程日志位于对应的 `*-logs/` 目录;分析产物为 `comparison-summary.json`、`comparison-summary.csv` 与 `comparison-summary.md`。
|
||||
|
||||
## 实际选层与卸载证据
|
||||
|
||||
每条记录的选层不是由 analyzer 按标签公式补写。只读证据 hook 从真实运行时 offloader 对象捕获 `selected_layers`、`wrapped_count` 和 `offloaded_bytes`,同时在对应过程日志中发布恰好一条结构化证据;record、sidecar 和 log payload 必须完全一致才会发布记录。
|
||||
|
||||
- 45 个 current/native 正参数进程实际类为 `vllm_ascend.model_executor.offloader.prefetch.AscendPrefetchOffloader`。
|
||||
- 36 个 official latest 正参数进程实际类为 `vllm_ascend.model_executor.offloader.prefetch.NPUPrefetchOffloader`。
|
||||
- 9 个 baseline 进程具有明确空选择证据:类为 null、`selected_layers=[]`、`wrapped_count=0`、`offloaded_bytes=0`。
|
||||
- 全部 81 个正参数进程的实际选层列表、包裹数和卸载字节均与参数标签及模型层字节身份匹配。例如 `(28,1)` 为 `[27]`、1 个包裹、466,115,584 bytes;`(14,1)` 为 `[13,27]`、2 个包裹、932,231,168 bytes。
|
||||
|
||||
因此,下表的选中层数和释放字节由真实运行时证据确认,而非仅由报告端推导。
|
||||
|
||||
## 同运行时:当前实现与官方 latest
|
||||
|
||||
下表严格按 `comparison-summary.json` 的参数顺序列出。current 与 latest 均运行在 v0.23.0rc1/CANN 9.0.1 环境;可用 KV cache 和吞吐取 3 次进程中位数,吞吐保留以同阶段 baseline `522.718550897524 tok/s` 为分母。
|
||||
|
||||
| 参数 `(group,num)` | 选中层数 | 释放权重(bytes / GiB) | current KV cache(GiB) | current(tok/s) | latest(tok/s) | current - latest | 吞吐保留 |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `(2,1)` | 14 | 6,525,618,176 / 6.077 | 21.73 | 33.895563 | 33.894397 | +0.003440% | 6.484477% |
|
||||
| `(4,1)` | 7 | 3,262,809,088 / 3.039 | 18.69 | 67.689144 | 67.718892 | -0.043928% | 12.949444% |
|
||||
| `(4,2)` | 14 | 6,525,618,176 / 6.077 | 21.73 | 33.893602 | 33.908388 | -0.043607% | 6.484102% |
|
||||
| `(7,1)` | 4 | 1,864,462,336 / 1.736 | 17.39 | 118.301499 | 118.319756 | -0.015431% | 22.631969% |
|
||||
| `(8,1)` | 3 | 1,398,346,752 / 1.302 | 16.96 | 157.486791 | 157.514203 | -0.017403% | 30.128411% |
|
||||
| `(8,2)` | 6 | 2,796,693,504 / 2.605 | 18.26 | 78.990658 | 78.980649 | +0.012673% | 15.111508% |
|
||||
| `(14,1)` | 2 | 932,231,168 / 0.868 | 16.52 | 233.570208 | 233.467831 | +0.043851% | 44.683742% |
|
||||
| `(14,2)` | 4 | 1,864,462,336 / 1.736 | 17.39 | 117.837780 | 117.836097 | +0.001429% | 22.543256% |
|
||||
| `(28,1)` | 1 | 466,115,584 / 0.434 | 16.09 | 448.335942 | 448.700340 | -0.081212% | 85.770046% |
|
||||
| `(28,2)` | 2 | 932,231,168 / 0.868 | 16.52 | 230.894931 | 231.095048 | -0.086595% | 44.171941% |
|
||||
|
||||
baseline 不选择层、也不释放权重,KV cache 中位数为 15.66 GiB,吞吐为 522.718551 tok/s。按 analyzer 对容量和吞吐中位数执行的严格支配判定,current frontier 为 baseline 加 `(28,1)`、`(14,1)`、`(8,1)`、`(7,1)`、`(8,2)`、`(4,1)`、`(2,1)`。official latest 只在同为 14 层的等容量点上选择 `(4,2)` 而非 `(2,1)`;两者吞吐差仅约 0.041%,说明这种严格分类会被微小运行变化影响,不应解释为稳定的选层模式优势。
|
||||
|
||||
严格 frontier 只是对本次中位数的数学分类,不表示相邻小差异具有统计稳定性。`(28,1)` 将可用 KV cache 从 15.66 GiB 提高到 16.09 GiB,约增加 0.434 GiB,同时保留 85.770046% 吞吐,是实践膝点。`(14,1)` 达到 16.52 GiB,但吞吐仅保留 44.683742%,只应在容量优先且 baseline、`(28,1)` 均不满足时选择。
|
||||
|
||||
## 低密度长窗口复测
|
||||
|
||||
长窗口以 10 个 measured rounds/进程复测低密度点。current baseline 为 `524.121425 tok/s`、可用 KV cache 15.66 GiB;下面两行均为 3 个进程的中位数。
|
||||
|
||||
| 参数 | 释放权重(GiB) | KV cache(GiB) | current(tok/s) | latest(tok/s) | current - latest | 吞吐保留 |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| `(14,1)` | 0.868 | 16.52 | 233.490995 | 233.483712 | +0.003119% | 44.549027% |
|
||||
| `(28,1)` | 0.434 | 16.09 | 448.176917 | 449.523094 | -0.299468% | 85.510131% |
|
||||
|
||||
`(14,1)` 在短、长窗口分别为 +0.043851% 和 +0.003119%,方向相同但幅度极小。短窗口 current/latest 三进程相对范围分别为 0.038683%/0.061197%,长窗口分别为 0.067527%/0.031453%;该差值未明显超出进程间变化,不能据此认定实现带来速度提升。
|
||||
|
||||
`(28,1)` 短窗口为 -0.081212%,长窗口为 -0.299468%,两阶段均未优于 latest。其短窗口 current/latest 三进程相对范围分别为 0.383038%/0.411468%,长窗口分别为 0.339707%/0.051839%;差值仍需结合运行变化解释,但无论如何没有形成 current-over-latest 的速度优势。
|
||||
|
||||
## 同一选中层数:间隔分组与相邻分组
|
||||
|
||||
以下均取同运行时 current 中位数。每对释放权重和可用 KV cache 相同,前者的层间隔更大。
|
||||
|
||||
| 相同选中层数 | KV cache(GiB) | 前者吞吐(tok/s) | 后者吞吐(tok/s) | 前者相对后者 |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| `(14,1)` vs `(28,2)`,2 层 | 16.52 / 16.52 | 233.570208 | 230.894931 | +1.158656% |
|
||||
| `(7,1)` vs `(14,2)`,4 层 | 17.39 / 17.39 | 118.301499 | 117.837780 | +0.393523% |
|
||||
| `(2,1)` vs `(4,2)`,14 层 | 21.73 / 21.73 | 33.895563 | 33.893602 | +0.005786% |
|
||||
|
||||
这些同层数差异随密度缩小,且没有独立统计设计证明选择模式本身造成速度差;它们只描述本次参数空间,不能据此宣称一种选层间隔稳定更快。
|
||||
|
||||
## native v0.18 与 v0.23-current 的辅助对照
|
||||
|
||||
下表用 hardened-v6 `comparison-summary.json` 中 `phases.native-v018` 与 `phases.same-runtime` 的同名 label 直接重算。v0.23 baseline 未启用覆盖 offloader,仍是 official baseline。
|
||||
|
||||
| 配置 | native v0.18(tok/s) | v0.23-current(tok/s) | v0.23 - v0.18(tok/s) | v0.23 - v0.18 |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| baseline | 526.321482 | 522.718551 | -3.602931 | -0.684550% |
|
||||
| `(28,1)` | 449.420321 | 448.335942 | -1.084379 | -0.241284% |
|
||||
| `(14,1)` | 233.381710 | 233.570208 | +0.188499 | +0.080768% |
|
||||
| `(8,1)` | 157.450692 | 157.486791 | +0.036099 | +0.022927% |
|
||||
|
||||
该表混合了 vLLM、vLLM Ascend、Torch、Python/打包状态和相关运行栈版本,不能将差异归因于 offloader。它只提供跨栈辅助观察;同运行时 current/latest 配对才是主要比较证据。
|
||||
|
||||
## 历史运行隔离与 v6 身份加固
|
||||
|
||||
hardened-v4 已完成 63 个同运行时和 15 个长窗口进程,但在任何 native v0.18 case 之前由 native byte-hash guard fail closed。根因是旧 v0.18 部署的 offloader 文件使用 LF,而精确项目归档使用 CRLF:换行规范化后的代码内容完全一致,但 bytewise tree SHA-256 分别为 `742846842b8d32b007997744a6d70d3bb32a98fb711556add294e21acada41d2` 与 `0f864b7af056732895677053c1b215fae49b730634506a1a529ac4693e1f0f5f`。
|
||||
|
||||
v4 目录被原样保留且没有续写,其 78 条测量不进入本报告。hardened-v5 完成了 90 进程矩阵,但后续审查指出“调用者可替换 CANN 路径”和“实际 latest package/source 未与固定完整树身份绑定”两个证据缺口,因此 v5 数据也不作为本文结果。commit `ec673f8` 将 CANN 权威文件与环境脚本、latest wheel/安装包树、latest 源码树、native v0.18 完整源码树、两个 venv 激活脚本、项目 worker/offloader/runner 全部固定并 fail closed;随后以三个全新 owned roots 完整重跑 hardened-v6。post guard 再次以全新 roots 验证全部身份,RC 为 0。
|
||||
|
||||
## 验证、限制与预报告归档
|
||||
|
||||
- `validation.json` 为 `valid: true`,`model_files_verified: true`,并核对 63 + 15 + 12 = 90 条记录及相同数量日志;90 条 runtime evidence 全部存在且匹配。
|
||||
- `independent-verification.log` 从原始记录重算 30 个 labels,得到 `MAX_PROCESS_DIFF=0.000000000000`、`MAX_POOLED_DIFF=0.000000000000`、`MAX_COMPARISON_DIFF=0.000000000000`。
|
||||
- 远端显式列出的 8 个非 Windows offloader 文件为 **87 passed in 18.89s**;controller 本地 focused suite 为 **41 tests passed**。远端整目录诊断收集为 **113 passed、15 failed**,15 个失败全部来自 Windows 本地路径/打包流程断言,因此不将它们误计为 Linux 运行时失败。
|
||||
- runner 初始 preliminary manifest 为 **129/129**。加入 formal driver、远端 offloader test、独立重算、post guard 与后验 NPU 证据后,预报告 `SHA256SUMS` 为 **137/137**;远端和本地逐条校验均为 RC 0。
|
||||
- 预报告归档 `qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8-pre-report.tar.gz` 的 SHA-256 为 `d09ae2cc7aad493c887c3b2fb3d047972d6f8f6b6c2205d7a187af659edc8b4a`。该值只标识加入本文之前的不可变证据包。
|
||||
- 后验 `npu-smi` 显示 NPU 6 health OK、无运行中进程。全部 90 个日志均无 `Traceback`。
|
||||
|
||||
两套环境的 `pip freeze` 仍显示 stale editable-Git metadata 警告,无法从已失效的 vLLM editable worktree 补充 VCS URI。身份判定不依赖这些易漂移 URI,而以 importlib 包版本、wheel 与安装包完整树同哈希、latest/native 完整源码树、模型/CANN 权威文件和固定脚本 hash 为准。
|
||||
|
||||
native v0.18 的 12/12 日志出现 `Failed to import vllm_ascend_C ... Sleep mode will be disabled`。这表示 sleep mode 被关闭;它不等同于 offloader 失败,但进一步限制跨栈辅助表的解释。精度未在本次 7B 性能 run 中测试,本文不虚构 accuracy 结果。
|
||||
|
||||
最终交付采用固定打包流程:controller 将本报告所在最终提交的 tracked 报告加入 hardened-v6 结果目录,重新生成并远端校验 final `SHA256SUMS`,再创建最终归档、下载并在本地复核 manifest 和报告内容。本报告不记录包含自身的 final 顶层 SHA-256;该值由外部交付记录承载,以避免自引用。
|
||||
|
||||
## 精确复现命令
|
||||
|
||||
在部署了上述精确源码、模型和两个 venv 的服务器上,先确认下面三个 runner-owned roots 均不存在:
|
||||
|
||||
```bash
|
||||
/data/reproduction-results/qwen25-7b-current-vs-latest-20260731-rerun
|
||||
/data/reproduction-results/qwen25-7b-current-vs-latest-20260731-rerun.current_impl_overlay
|
||||
/data/reproduction-results/qwen25-7b-current-vs-latest-20260731-rerun.cache
|
||||
```
|
||||
|
||||
然后从精确项目根执行:
|
||||
|
||||
```bash
|
||||
cd /data/ccf-vllm-ascend-ec673f8-exact
|
||||
RESULTS_DIR=/data/reproduction-results/qwen25-7b-current-vs-latest-20260731-rerun \
|
||||
bash /data/ccf-vllm-ascend-ec673f8-exact/scripts/run_large_model_parameter_comparison.sh
|
||||
```
|
||||
|
||||
runner 在任何 benchmark 写入前确认 `RESULTS_DIR`、`${RESULTS_DIR}.current_impl_overlay` 和 `${RESULTS_DIR}.cache` 均不存在,再分别以 fail-closed `mkdir` 占有;任一路径已存在或任一创建失败即停止。CANN 权威文件/环境脚本及其 hash、两个固定源码 root 及完整树 hash、latest wheel/安装包树、两个 venv 激活脚本、模型 revision/清单和项目 worker/offloader/runner 均在首个 case 前核验,调用者不能替换。继承的 cache 值和 `PYTHONPATH` 不会进入正式 case:cache 固定绑定到本次唯一 `${RESULTS_DIR}.cache`,case `PYTHONPATH` 只包含证据 hook、选定 package root 和受信 CANN Python paths。`VLLM_NO_USAGE_STATS=1` 被无条件设置并记录。完整运行顺序与复现材料见 `formal-driver.log`、`runner.log` 和 `reproduction/`。
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,696 @@
|
|||
# [功能] 适配 offload_group_size 和 offload_num_in_group 到 Ascend NPU
|
||||
|
||||
## vLLM Ascend 权重分组预取卸载参数适配与优化
|
||||
|
||||
**团队名称:** 乘风破浪的大学生
|
||||
|
||||
**目标版本:** vLLM Ascend v0.18.0
|
||||
|
||||
**项目分支:** `feature/offload-group-num-v018-optimized`
|
||||
|
||||
**代码与复现套件提交:** `851feb5`
|
||||
|
||||
**7B 实测部署提交:** `ec673f8`
|
||||
|
||||
**结项日期:** 2026 年 8 月
|
||||
|
||||
---
|
||||
|
||||
## 摘要
|
||||
|
||||
本项目面向 vLLM Ascend v0.18.0,适配 vLLM 权重卸载配置中的 `offload_group_size` 与 `offload_num_in_group` 两个参数。实现按 decoder layer 分组,在每组末尾选择固定数量的层,将权重保存在 CPU 侧,并通过 Ascend NPU 独立复制流、事件同步和静态缓冲槽位在执行前进行预取。目标是在 NPU HBM 紧张时提供可配置、可复现的容量与吞吐权衡,而不是无条件提升吞吐。
|
||||
|
||||
在 `Qwen2.5-1.5B-Instruct` 的正式 `qwen-chat-v1` 精度协议下,`baseline_0_1`、`group_8_num_1` 与 `group_8_num_2` 的 GSM8K 均为 953/1319(72.2517%),GPQA Diamond 均为 47/198(23.7374%);两个 offload case 相对 baseline 的精度下降均为 0,且同数据集逐题输出 SHA-256 一致。
|
||||
|
||||
独立的 `Qwen/Qwen2.5-7B-Instruct` 性能证据覆盖 63 个同运行时进程、15 个长窗口进程和 12 个 native v0.18 辅助观察进程,共 90 条记录。当前负载下,`(28,1)` 释放约 0.434 GiB 权重容量,将可用 KV cache 提升至 16.09 GiB,并保留约 85.77% 吞吐,是实际容量膝点。同运行时 current/latest 的中位数差范围为 -0.086595% 至 +0.043851%,未确认当前实现相对 official latest 存在稳定速度优势;7B 运行未采集精度结果。
|
||||
|
||||
项目同时完成参数语义与边界校验、Ascend Prefetch Offloader、官方 `auto/prefetch/uva` 后端契约兼容、CPU pinned storage 能力探测、shape/stride/dtype 保真、设备健康检查、运行时证据 hook、身份绑定和独立重算。实际使用应按模型可用 HBM、KV cache 需求和吞吐目标选取参数,不能将 offload 简化为单一性能开关。
|
||||
|
||||
**关键词:** vLLM Ascend;权重卸载;预取;NPU;HBM;GSM8K;GPQA
|
||||
|
||||
---
|
||||
|
||||
# 一、项目概述
|
||||
|
||||
## 1.1 项目背景
|
||||
|
||||
大模型推理的设备内存通常同时承载模型权重、KV cache、激活值和运行时工作区。随着模型规模、上下文长度和并发数增长,权重常驻空间会压缩 KV cache 容量,进而限制模型可加载规模或服务并发能力。
|
||||
|
||||
vLLM 提供 CPU weight offload 能力。`offload_group_size` 与 `offload_num_in_group` 用于控制以 decoder layer 为单位的分组卸载:
|
||||
|
||||
| 参数 | 含义 | 默认/禁用行为 |
|
||||
| --- | --- | --- |
|
||||
| `offload_group_size` | 每多少层组成一个 offload group | 0 表示不启用分组预取卸载 |
|
||||
| `offload_num_in_group` | 每个 group 中选择多少个末尾层进行卸载 | 0 表示禁用,且不得大于 group size |
|
||||
|
||||
本项目认领这两个参数,并将其适配到 vLLM Ascend v0.18.0。目标基线扫描未发现 Ascend 专用实现,因此本项目在该基线上补齐参数解释、分层选择、权重驻留、NPU 预取和测试复现链路。
|
||||
|
||||
## 1.2 目标与范围
|
||||
|
||||
项目目标包括:
|
||||
|
||||
1. 在 vLLM Ascend v0.18.0 中正确读取和应用两个参数。
|
||||
2. 保持参数为 0 时的原有行为,不破坏 vLLM 既有 UVA offloader。
|
||||
3. 使用 Ascend NPU Stream/Event 实现可复用的权重预取路径。
|
||||
4. 为两个参数分别提供边界、选择规则和 runner 集成测试。
|
||||
5. 使用 GSM8K 和 GPQA Diamond 完成三组参数矩阵的全量精度验证。
|
||||
6. 提供性能、精度和统一复现脚本,确保环境、命令和产物可追踪。
|
||||
|
||||
本项目不把以下内容作为已完成范围:
|
||||
|
||||
- 多卡和分布式推理下的物理设备映射闭环。
|
||||
- server streaming 场景下的真实 TTFT 测量。
|
||||
- 长上下文、高并发和 ACL graph 的系统性能结论。
|
||||
- 对所有模型、提示词或数据集作无精度损失的普遍保证。
|
||||
|
||||
## 1.3 版本背景与主仓实现情况
|
||||
|
||||
本项目的竞赛目标版本固定为 vLLM Ascend v0.18.0,源码基线提交为:
|
||||
|
||||
```text
|
||||
72dc68973bd7e6ceef9a122de316a7ee9c9a84aa
|
||||
```
|
||||
|
||||
基线扫描未发现 `offload_group_size` 与 `offload_num_in_group` 的 Ascend 专用执行路径,因此竞赛规则中“若主仓已有实现,须在性能或精度至少一个维度取得可量化提升”的附加要求不适用于本目标基线。
|
||||
|
||||
前期版本调研显示,后续 vLLM Ascend 版本已逐步出现官方 NPU Prefetch Offloader,其中首个包含相关实现的发布序列为 v0.21.0rc1。由于本项目必须继续基于 v0.18.0,工作重点是将参数语义、后端兼容和复现体系稳定适配回目标版本,而不是直接依赖后续主仓实现。
|
||||
|
||||
## 1.4 开发历程与证据演进
|
||||
|
||||
决赛材料按以下四个阶段组织,避免将不同模型、协议或运行时的数据混为同一次实验:
|
||||
|
||||
1. **v0.18.0 参数适配。** 在竞赛目标版本实现 `offload_group_size` 与 `offload_num_in_group` 的选择、预取、runner 集成和边界测试。
|
||||
2. **`qwen-chat-v1` 精度协议修复。** 固定 chat template、样本数、生成预算和评分规则,使用 1.5B 全量 GSM8K/GPQA 矩阵确认两个 offload case 的零额外精度下降。
|
||||
3. **Qwen2.5-7B 同运行时 current/latest 对比。** 在相同 v0.23.0rc1/CANN 9.0.1 运行时中比较 current 与 official latest,重点验证容量/吞吐权衡,不从该实验推导精度结论。
|
||||
4. **hardened-v6 身份绑定与独立重算。** 将 CANN、package/source tree、模型、runner 和部署身份 fail-closed 绑定,并从原始记录独立重算结果与比较差值。
|
||||
|
||||
## 1.5 决赛阶段工作定位
|
||||
|
||||
当前核心 offloader 文件与初赛最终提交包文本等价,因此本结项书不把核心算法表述为“决赛重新实现”或“相对初赛新增提速”。决赛阶段的实质工作集中在:
|
||||
|
||||
- 固定并验证 vLLM Ascend v0.18.0 与 CANN 8.5.1 运行环境。
|
||||
- 对齐官方 `offload_backend=auto/prefetch/uva` 契约,避免非标准环境变量切换。
|
||||
- 完善 pinned storage、布局保真、设备采样和健康门禁等工程细节。
|
||||
- 建立三组参数 case 的正式性能和精度矩阵。
|
||||
- 形成从安装、测试、性能、精度到结果校验的一体化复现入口。
|
||||
- 明确数据收益、吞吐代价和证据限制,避免只报告“运行成功”。
|
||||
|
||||
---
|
||||
|
||||
# 二、参数理解与技术难点
|
||||
|
||||
## 2.1 参数选择语义
|
||||
|
||||
对 layer index 为 `i` 的 decoder layer,选择规则为:
|
||||
|
||||
```text
|
||||
i % offload_group_size >= offload_group_size - offload_num_in_group
|
||||
```
|
||||
|
||||
也就是说,每个 group 选择末尾连续的 `offload_num_in_group` 层。以 24 层模型为例:
|
||||
|
||||
| Case | group size | num in group | 选中层 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| `baseline_0_1` | 0 | 1 | 无 |
|
||||
| `group_8_num_1` | 8 | 1 | 7、15、23 |
|
||||
| `group_8_num_2` | 8 | 2 | 6、7、14、15、22、23 |
|
||||
|
||||
这种规则是确定性的固定选择,不是根据运行时负载自动搜索最优层。
|
||||
|
||||
## 2.2 参数边界
|
||||
|
||||
实现需要保证:
|
||||
|
||||
- 两个参数必须为非负整数。
|
||||
- `offload_group_size=0` 或 `offload_num_in_group=0` 时禁用分组预取卸载。
|
||||
- `offload_num_in_group` 不得大于 `offload_group_size`。
|
||||
- 参数禁用时,不应覆盖 vLLM 父类已经构造的 offloader。
|
||||
- 非法后端名称应尽早抛出明确错误。
|
||||
|
||||
需要特别说明:配置层只接受 `auto`、`prefetch` 和 `uva`。仓库中保留了同步 offloader 作为内部参考和直接测试对象,但用户不能通过 `offload_backend=sync` 选择它。
|
||||
|
||||
## 2.3 模型层命名与选择
|
||||
|
||||
参数语义面向 decoder layers,但运行时模块由完整模块名标识。实现需要稳定提取 layer index,并确保:
|
||||
|
||||
- 非 decoder 模块不会被误选。
|
||||
- 同一层内多个子模块的 wrap 行为一致。
|
||||
- 分组选择能在模型加载完成前确定。
|
||||
- 测试可直接验证给定层数下的精确选择集合。
|
||||
|
||||
## 2.4 CPU-NPU 权重生命周期
|
||||
|
||||
选中层的权重不能在每次前向时临时创建,否则会引入额外分配、碎片和同步开销。实现需要管理三个阶段:
|
||||
|
||||
1. 模型加载后,将选中参数捕获到 CPU storage。
|
||||
2. 初始化固定数量的 NPU static buffer slot。
|
||||
3. 前向执行前将当前层权重预取到 slot,执行后滚动复用 slot。
|
||||
|
||||
CPU storage 与 NPU static buffer 必须保持原始 `shape`、`stride()` 和 `dtype`。如果只按元素数量分配连续 buffer,非连续布局或不同 dtype 可能出现错误复用。
|
||||
|
||||
## 2.5 异步复制与正确性
|
||||
|
||||
预取并不等于“完全无等待”。NPU copy stream 可以与部分计算重叠,但在目标层执行前仍需等待对应 event。正确性要求:
|
||||
|
||||
- copy 在独立 NPU stream 上提交。
|
||||
- copy 完成后记录 event。
|
||||
- 当前计算 stream 在使用权重前等待 event。
|
||||
- 前向结束后启动未来层预取。
|
||||
- 推理结束或 teardown 时等待 copy stream,避免缓冲被提前释放。
|
||||
|
||||
因此,方案的实际收益依赖可重叠计算量、PCIe/互联带宽、层大小和预取距离。
|
||||
|
||||
## 2.6 HBM 指标解释
|
||||
|
||||
权重卸载释放的空间可能被 vLLM 重新分配给 KV cache。生成结束后直接比较 `npu-smi` 已用 HBM,可能看不到与权重节省相同的下降。项目采用两个独立口径:
|
||||
|
||||
- 模型加载日志:记录权重 storage 节省。
|
||||
- vLLM KV cache 规划日志:记录可用 KV cache 容量。
|
||||
|
||||
这比单纯报告进程结束时的设备显存差值更符合 vLLM 的内存分配行为。
|
||||
|
||||
---
|
||||
|
||||
# 三、技术方案
|
||||
|
||||
## 3.1 总体架构
|
||||
|
||||
```text
|
||||
OffloadConfig
|
||||
|
|
||||
| offload_group_size / offload_num_in_group / offload_backend
|
||||
v
|
||||
model_runner_v1.py
|
||||
|
|
||||
+-- uva 或禁用:保留父类 offloader
|
||||
|
|
||||
+-- auto/prefetch + 有效参数
|
||||
|
|
||||
v
|
||||
AscendPrefetchOffloader
|
||||
|
|
||||
+-- layer selection
|
||||
+-- CPU weight storage
|
||||
+-- static NPU buffer pool
|
||||
+-- NPU copy stream + events
|
||||
+-- forward hooks / rolling prefetch
|
||||
```
|
||||
|
||||
## 3.2 模块划分
|
||||
|
||||
| 模块 | 职责 |
|
||||
| --- | --- |
|
||||
| `vllm_ascend/model_executor/offloader/selection.py` | 校验参数、解析 layer index、生成选中层集合 |
|
||||
| `vllm_ascend/model_executor/offloader/prefetch.py` | CPU storage、静态 NPU buffer、Stream/Event、预取 hook |
|
||||
| `vllm_ascend/model_executor/offloader/sync.py` | 同步参考路径和直接测试对象 |
|
||||
| `vllm_ascend/model_executor/offloader/__init__.py` | 公共接口导出 |
|
||||
| `vllm_ascend/worker/model_runner_v1.py` | offloader 工厂、runner 注册、加载后初始化 |
|
||||
|
||||
## 3.3 后端选择兼容
|
||||
|
||||
工厂行为如下:
|
||||
|
||||
| `offload_backend` | 参数状态 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| `uva` | 任意 | 保留父类 vLLM UVA offloader |
|
||||
| `auto` | group 或 num 为 0 | 保留父类行为 |
|
||||
| `auto` | group 与 num 均有效 | 创建 Ascend Prefetch Offloader |
|
||||
| `prefetch` | 参数有效 | 创建 Ascend Prefetch Offloader |
|
||||
| 其他值 | 任意 | 抛出不支持错误 |
|
||||
|
||||
该兼容方式避免了两个风险:
|
||||
|
||||
- 在不启用参数时错误替换父类 offloader。
|
||||
- 引入未文档化环境变量,造成配置入口与 vLLM 官方契约分离。
|
||||
|
||||
## 3.4 CPU storage 与静态缓冲
|
||||
|
||||
Prefetch 路径通过以下逻辑创建 CPU storage:
|
||||
|
||||
```python
|
||||
torch.empty_strided(
|
||||
tensor.shape,
|
||||
tensor.stride(),
|
||||
dtype=tensor.dtype,
|
||||
device="cpu",
|
||||
pin_memory=is_pin_memory_available(),
|
||||
)
|
||||
```
|
||||
|
||||
随后复制模型加载后已经处理完成的权重。NPU buffer key 包含参数名、shape、stride 和 dtype,防止不同权重错误共享同一 slot。
|
||||
|
||||
此处只能准确表述为“平台报告支持时使用 pinned CPU storage”。不能承诺所有平台始终启用 pin memory,也不能仅凭该分配方式推导出完全非阻塞 DMA。
|
||||
|
||||
## 3.5 Stream/Event 预取流程
|
||||
|
||||
对当前目标模块,执行流程为:
|
||||
|
||||
1. 在 copy stream 上将 CPU 权重复制到对应 static buffer。
|
||||
2. copy 完成后记录 NPU event。
|
||||
3. 当前计算 stream 在模块前向前等待该 event。
|
||||
4. 将 buffer 中的权重临时绑定到目标参数。
|
||||
5. 前向完成后,根据预取窗口启动后续模块的 copy。
|
||||
6. slot 在生命周期结束后回收,用于后续层。
|
||||
|
||||
该流程减少了运行期动态 buffer 分配,并为计算与传输重叠提供基础。由于存在显式 event wait,性能结果必须由实际 benchmark 给出,不能仅根据结构推断“零开销”。
|
||||
|
||||
## 3.6 设备映射和健康门禁
|
||||
|
||||
性能与精度脚本默认设置:
|
||||
|
||||
```bash
|
||||
export ASCEND_RT_VISIBLE_DEVICES=0
|
||||
```
|
||||
|
||||
脚本通过 `npu-smi info -m` 获取物理 NPU index,并将同一 index 用于健康检查和 HBM 采样。正式运行前执行:
|
||||
|
||||
```bash
|
||||
bash scripts/check_npu_health.sh
|
||||
```
|
||||
|
||||
检查未返回 `Health Status : OK` 时以状态码 42 终止。
|
||||
|
||||
当前探测实现取映射输出中的第一张 Ascend 卡。在多卡、卡重排或 `ASCEND_RT_VISIBLE_DEVICES` 指向非首卡时,应显式设置:
|
||||
|
||||
```bash
|
||||
export ASCEND_RT_VISIBLE_DEVICES=0
|
||||
export NPU_INDEX=0
|
||||
export NPU_CHIP_INDEX=0
|
||||
```
|
||||
|
||||
并将三个值写入复现记录。
|
||||
|
||||
## 3.7 运行时证据与身份绑定
|
||||
|
||||
7B 对比不由报告端按参数标签推导选层。只读 runtime evidence hook 直接从实际 offloader 对象捕获 `selected_layers`、`wrapped_count`、`offloaded_bytes` 和实际类名;每个进程的 record、sidecar 与过程日志中的结构化 payload 必须完全一致才发布记录。baseline 具有明确空选择证据,正参数 case 的实际类、选层列表、包裹数和卸载字节均可逐条核验。
|
||||
|
||||
正式 runner 在任何 benchmark 写入前执行 fail-closed 身份绑定:CANN 权威安装文件和环境脚本、official latest 的 wheel/package tree 与 native source tree、v0.18 source tree、模型 revision 与 SHA-256 清单、项目 worker/offloader/runner 均必须匹配固定 hash。调用者不可替换这些路径;任一检查失败即停止。该链路确保性能数字能够追溯到确定的运行时和模型,而不是仅依赖命令行标签或可漂移的 editable-Git 元数据。
|
||||
|
||||
## 3.8 异常与降级策略
|
||||
|
||||
- 参数非法:抛出明确 `ValueError`。
|
||||
- 后端非法:在 offloader 工厂阶段拒绝。
|
||||
- NPU 健康异常:正式脚本 fail-fast。
|
||||
- pin memory 不可用:使用普通 CPU storage,不中断推理。
|
||||
- 参数禁用:保留 vLLM 父类 offloader,不改变默认推理路径。
|
||||
- 可选 `msserviceprofiler` 缺失:记录非致命插件加载信息,不影响 Ascend 平台激活和推理。
|
||||
|
||||
---
|
||||
|
||||
# 四、实际交付
|
||||
|
||||
## 4.1 代码与测试
|
||||
|
||||
| 类型 | 路径 |
|
||||
| --- | --- |
|
||||
| 参数选择 | `vllm_ascend/model_executor/offloader/selection.py` |
|
||||
| Prefetch Offloader | `vllm_ascend/model_executor/offloader/prefetch.py` |
|
||||
| 同步参考实现 | `vllm_ascend/model_executor/offloader/sync.py` |
|
||||
| runner 集成 | `vllm_ascend/worker/model_runner_v1.py` |
|
||||
| 参数单测 | `tests/model_executor/offloader/test_offload_group_size_param.py` |
|
||||
| 参数单测 | `tests/model_executor/offloader/test_offload_num_in_group_param.py` |
|
||||
| 选择规则 | `tests/model_executor/offloader/test_selection.py` |
|
||||
| Prefetch 行为 | `tests/model_executor/offloader/test_prefetch.py` |
|
||||
| runner 集成 | `tests/model_executor/offloader/test_runner_integration.py` |
|
||||
| 精度复现结构 | `tests/model_executor/offloader/test_accuracy_repro.py` |
|
||||
| 脚本验收 | `tests/model_executor/offloader/test_acceptance_scripts.py` |
|
||||
| 7B 对比与身份绑定 | `tests/model_executor/offloader/test_large_model_parameter_comparison.py` |
|
||||
| 参数优化分析 | `tests/model_executor/offloader/test_parameter_optimization_analysis.py` |
|
||||
|
||||
## 4.2 性能与精度脚本
|
||||
|
||||
| 脚本 | 作用 |
|
||||
| --- | --- |
|
||||
| `scripts/run_benchmark_all.sh` | 运行三组正式性能 case |
|
||||
| `scripts/bench_one.py` | 执行单次推理并采集吞吐、延迟和 HBM 证据 |
|
||||
| `scripts/run_accuracy_gsm8k_gpqa.sh` | 运行 GSM8K 与 GPQA 三组精度矩阵 |
|
||||
| `scripts/accuracy_gsm8k_gpqa.py` | 数据加载、推理、评分和 example JSONL 输出 |
|
||||
| `scripts/verify_accuracy_matrix.py` | 强制正式协议、样本数、数据集哈希、模型/seed 一致性和零精度下降 |
|
||||
| `scripts/run_reproduce_all.sh` | 性能后精度的一键复现入口 |
|
||||
| `scripts/check_npu_health.sh` | 正式测试前 NPU 健康门禁 |
|
||||
| `scripts/detect_npu_index.py` | 获取物理 NPU index |
|
||||
| `scripts/install_v018_runtime_compat.sh` | 安装并重建 v0.18.0 兼容运行环境 |
|
||||
| `scripts/run_large_model_parameter_comparison.sh` | fail-closed 运行 7B current/latest 对比并生成运行时证据 |
|
||||
| `scripts/analyze_large_model_parameter_comparison.py` | 从原始 7B 记录重算容量、吞吐与 current/latest 差值 |
|
||||
| `scripts/run_current_vs_latest_performance.sh` | 按当前实现与 official latest 的固定环境运行对比入口 |
|
||||
| `scripts/run_parameter_optimization_benchmark.sh` | 运行 v0.18 参数容量/吞吐矩阵 |
|
||||
|
||||
## 4.3 文档交付
|
||||
|
||||
| 文档 | 内容 |
|
||||
| --- | --- |
|
||||
| `docs/v018-deployment-and-reproduction.md` | 环境部署、性能、精度和统一复现 |
|
||||
| `docs/v018-test-evidence.md` | 正式测试结果及健康状态 |
|
||||
| `docs/offload-group-size/` | 参数技术方案、单测计划和测试报告 |
|
||||
| `docs/offload-num-in-group/` | 参数技术方案、单测计划和测试报告 |
|
||||
| `docs/finals/` | 决赛结项书和答辩材料 |
|
||||
|
||||
---
|
||||
|
||||
# 五、测试结论
|
||||
|
||||
## 5.1 测试环境与模型边界
|
||||
|
||||
本章使用两套互不混用的证据。1.5B 证据只回答 offload 是否引入额外精度下降;7B 证据只回答固定负载下的容量/吞吐权衡与 current/latest 比较,不将 7B 性能 run 表述为精度测试。
|
||||
|
||||
| 证据 | 模型与运行边界 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| 1.5B 精度 | `Qwen2.5-1.5B-Instruct`,vLLM Ascend v0.18.0,正式 `qwen-chat-v1` 协议 | GSM8K/GPQA 的零额外精度下降 |
|
||||
| 7B 同运行时 | `Qwen/Qwen2.5-7B-Instruct`,28 个 decoder layers,FP16,Ascend 910B2C,CANN 9.0.1,v0.23.0rc1 | current 与 official latest 的公平容量/吞吐比较 |
|
||||
| 7B native 辅助观察 | 同一 7B 模型,native v0.18 源码与固定运行栈 | 跨栈辅助观察,不作 current/latest 主结论 |
|
||||
|
||||
7B 部署身份为 `ec673f8`;代码和复现套件身份为 `851feb5`。latest、native、CANN、模型、wheel/package tree 和 runner 的固定 hash 在启动时校验,后续文档提交不作为代码身份。
|
||||
|
||||
## 5.2 功能与证据测试
|
||||
|
||||
- hardened-v6 远端明确列出的 8 个非 Windows offloader 测试文件:`87 passed in 18.89s`。
|
||||
- 本地 focused suite:`41 tests passed`。
|
||||
- runtime evidence hook 对每个 7B 进程记录实际 offloader 类、`selected_layers`、`wrapped_count` 和 `offloaded_bytes`;record、sidecar 与过程日志必须一致。
|
||||
- `validation.json` 检查为 `valid: true`、`model_files_verified: true`,并确认 63 + 15 + 12 = 90 条记录、90 份独立日志和 90 份运行时证据。
|
||||
|
||||
远端整目录诊断中的 15 个失败来自 Windows 本地路径或打包流程断言,不计为 Linux/Ascend 运行时失败。独立重算从原始记录得到 `MAX_PROCESS_DIFF=0.000000000000`、`MAX_POOLED_DIFF=0.000000000000` 和 `MAX_COMPARISON_DIFF=0.000000000000`。
|
||||
|
||||
## 5.3 1.5B GSM8K/GPQA
|
||||
|
||||
正式精度设置:
|
||||
|
||||
```text
|
||||
prompt_protocol=qwen-chat-v1
|
||||
chat_template=true
|
||||
ACCURACY_LIMIT=0
|
||||
temperature=0
|
||||
seed=0
|
||||
GSM8K_MAX_TOKENS=512
|
||||
GPQA_MAX_TOKENS=8
|
||||
MAX_ACCURACY_DROP=0
|
||||
```
|
||||
|
||||
| 数据集 | 样本数 | baseline | `group_8_num_1` | `group_8_num_2` | 精度变化 |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| GSM8K test | 1319 | 953/1319(72.2517%) | 953/1319(72.2517%) | 953/1319(72.2517%) | 0 |
|
||||
| GPQA Diamond | 198 | 47/198(23.7374%) | 47/198(23.7374%) | 47/198(23.7374%) | 0 |
|
||||
|
||||
验证汇总返回:
|
||||
|
||||
```text
|
||||
verification_mode: formal
|
||||
all_passed: true
|
||||
```
|
||||
|
||||
正式模式不再把 baseline 的任意元数据直接视为权威,而是强制 1319/198 样本数、固定数据集 SHA-256、`qwen-chat-v1`、temperature 0、seed 0、512/8-token 预算,并检查三组 case 的 model、seed 和 dataset source 完全一致。`ACCURACY_LIMIT>0` 时脚本才显式启用 `--allow-limited`,且该模式只放宽样本数,不得作为提交成绩。
|
||||
|
||||
材料记录的 example JSONL SHA-256:
|
||||
|
||||
```text
|
||||
GSM8K:
|
||||
79f223c71ae7265bd37d7fc79a582f3b896823ba8a0256b52cbe370df9267c20
|
||||
|
||||
GPQA:
|
||||
0f403ae99640dde7c6c27bd993da9acc62752b87a1f81a655ede98de058914aa
|
||||
```
|
||||
|
||||
三组 case 在每个数据集上记录相同哈希,说明保存的预测输出逐字节一致。该结论只表示 offload 未引入额外精度损失,不表示该 1.5B 模型的绝对任务精度可以泛化到其他模型或提示协议。
|
||||
|
||||
## 5.4 7B 参数容量/吞吐
|
||||
|
||||
7B 固定负载为每个进程 8 个 prompts、128 输入 tokens、64 最大输出 tokens、`max_model_len=512`、`gpu_memory_utilization=0.5` 和 seed 0;各 case 独立加载模型,按三次重复的进程级吞吐中位数统计。
|
||||
|
||||
| 参数 `(group,num)` | 实际选中层 | 释放权重 | 可用 KV cache | current 吞吐 | 吞吐保留 |
|
||||
| --- | --- | ---: | ---: | ---: | ---: |
|
||||
| baseline | `[]` | 0.000 GiB | 15.66 GiB | 522.718551 tok/s | 100.000000% |
|
||||
| `(28,1)` | `[27]` | 0.434 GiB | 16.09 GiB | 448.335942 tok/s | 85.770046% |
|
||||
| `(14,1)` | `[13,27]` | 0.868 GiB | 16.52 GiB | 233.570208 tok/s | 44.683742% |
|
||||
|
||||
`(28,1)` 的实际证据为 1 个包裹、466,115,584 offloaded bytes。它以约 0.434 GiB 容量换取约 85.77% 吞吐保留,是当前负载的实际容量膝点;容量要求更高时可评估 `(14,1)`,但其吞吐代价显著。该判断不把低密度点的微小波动表述为提速。
|
||||
|
||||
## 5.5 current/latest 公平对比
|
||||
|
||||
同运行时主比较将 current 与 official latest 固定在同一 v0.23.0rc1/CANN 9.0.1 运行时,仅覆盖项目 offloader。10 组正参数对的进程吞吐中位数差范围为 **-0.086595% 至 +0.043851%**;长窗口复测中,`(14,1)` 为 +0.003119%,`(28,1)` 为 -0.299468%。
|
||||
|
||||
这些微小正值小于或接近进程间运行变化,且 `(28,1)` 在短、长窗口均未优于 latest。因此,当前实现相对 official latest **未确认稳定速度优势**。主结论是可复现的容量/吞吐取舍与公平比较链路,而不是 current-over-latest 的性能领先。
|
||||
|
||||
## 5.6 native v0.18 辅助观察
|
||||
|
||||
native v0.18 的 12 个进程覆盖 baseline、`(28,1)`、`(14,1)` 和 `(8,1)`,用于确认目标版本路径在固定环境中的辅助表现。该对照混合 vLLM、vLLM Ascend、Torch、Python 和打包状态,不能将跨栈吞吐差异归因于 offloader;同运行时 current/latest 配对仍是唯一的主比较证据。
|
||||
|
||||
native 日志中的 sleep mode 禁用提示不等同于 offloader 失败,但进一步限制了跨栈表的解释范围。
|
||||
|
||||
## 5.7 归档与限制
|
||||
|
||||
最终 hardened-v6 结果归档为 `qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8-results.tar.gz`,SHA-256 为 `a8cc0ae4aa506d5c75f97d891001006889554e6b4b14603758e0a5e2014521f3`。其 `validation.json` 必须保持 `valid=true` 与 `model_files_verified=true`;正式复核还应检查 90 条记录、90 份日志、90 份 runtime evidence 和独立重算的零差值。
|
||||
|
||||
大型原始归档不纳入 Git。性能结论仅适用于单卡、固定输入输出长度和固定负载;该性能实验未采集 GSM8K 或 GPQA,因此不得从容量/吞吐结果推导精度结论。
|
||||
|
||||
---
|
||||
|
||||
# 六、AI 工具使用经验
|
||||
|
||||
## 6.1 参与环节
|
||||
|
||||
AI 工具用于:
|
||||
|
||||
- 阅读 vLLM、vLLM Ascend 的参数和 offloader 调用链。
|
||||
- 梳理 v0.18.0 与后续版本的实现差异。
|
||||
- 辅助设计参数边界、后端选择和 buffer 生命周期测试。
|
||||
- 分析远端依赖冲突、CANN 自定义算子和设备映射问题。
|
||||
- 生成性能、精度和统一复现脚本初稿。
|
||||
- 对测试结果、文档数字和演示材料进行交叉审计。
|
||||
|
||||
## 6.2 有效方法
|
||||
|
||||
1. **先固定基线再改动。** 明确源码提交、运行时包和 CANN 版本,避免把环境漂移误判为代码问题。
|
||||
2. **先跑最小验证再扩展。** 先通过参数选择和 mock 单测,再进行真实 NPU 冒烟,最后进行正式性能与精度矩阵。
|
||||
3. **让 AI 输出可证伪假设。** 对性能、内存和精度结论要求给出对应日志字段、脚本和判定条件。
|
||||
4. **独立审计数字。** 将代码实现和测试证据交给不同审计流程,重点检查夸大表述、旧数据混用和无法复算的结果。
|
||||
5. **保留人工判断。** 参数是否值得启用取决于容量目标和吞吐预算,不能由 AI 根据单一指标自动下结论。
|
||||
|
||||
## 6.3 典型问题与改进
|
||||
|
||||
- AI 容易把“异步 copy”写成“零等待”,实际代码仍有 event wait。
|
||||
- AI 容易把 HBM 用量差等同于权重节省,忽略 vLLM 会扩大 KV cache。
|
||||
- AI 容易把 GPQA 相对 baseline 的零下降写成“绝对精度优秀”,混淆模型能力与 offload 一致性。
|
||||
- AI 容易混用 7 月 5 日旧 benchmark 与 7 月 21 日正式中位数。
|
||||
|
||||
解决方式是将结论绑定到具体环境、case、数据源和限制条件,并对每个量化数字进行双重核对。
|
||||
|
||||
---
|
||||
|
||||
# 七、限制与后续工作
|
||||
|
||||
## 7.1 当前限制
|
||||
|
||||
1. 性能验证集中于单卡 eager/Python LLM API。
|
||||
2. 输入长度、输出长度和 prompt 数固定,未覆盖长上下文和高并发。
|
||||
3. 设备 index 自动探测只取首个 Ascend 映射,多卡需要显式配置。
|
||||
4. 健康检查可通过 `SKIP_NPU_HEALTH_CHECK=1` 跳过,正式复现必须记录该变量未设置。
|
||||
5. example JSONL 哈希尚未被 `verify_accuracy_matrix.py` 自动强制校验。
|
||||
6. 正式原始结果尚未进入当前 Git 仓库,必须按归档 hash 与 `validation.json` 复核。
|
||||
7. 大模型性能证据覆盖容量与吞吐,不包含 GSM8K 或 GPQA 精度运行。
|
||||
8. current/latest 的微小差异未建立稳定速度优势,不能据此推广为实现提速。
|
||||
9. 同步 offloader 不是 `offload_backend` 可选择的正式用户后端。
|
||||
|
||||
## 7.2 后续优化方向
|
||||
|
||||
### 方向一:完善证据链
|
||||
|
||||
- 将正式原始结果归档为版本化附件。
|
||||
- 在 `verify_accuracy_matrix.py` 中复算并校验 example JSONL SHA-256。
|
||||
- 在 `reproduction-manifest.json` 中写入设备映射、健康输出、数据集哈希和 Git dirty 状态。
|
||||
|
||||
### 方向二:多卡设备映射
|
||||
|
||||
- 解析 `ASCEND_RT_VISIBLE_DEVICES` 与 `npu-smi info -m` 的对应关系。
|
||||
- 对每个逻辑 rank 记录物理 NPU/chip index。
|
||||
- 增加多卡和卡重排测试。
|
||||
|
||||
### 方向三:性能覆盖
|
||||
|
||||
- 增加长上下文、多并发和不同 batch size。
|
||||
- 使用 OpenAI-compatible server streaming 测量真实 TTFT、TPOT 和端到端延迟。
|
||||
- 验证 ACL graph、tensor parallel 和 pipeline parallel。
|
||||
- 扩展至更大模型,验证在“baseline 无法装入、offload 可以装入”场景中的容量价值。
|
||||
|
||||
### 方向四:参数自动推荐
|
||||
|
||||
根据模型层大小、可用 HBM、目标 KV cache 和链路带宽,建立离线估算器,输出候选 `group_size/num_in_group` 组合。推荐器应以“满足容量约束后最大化吞吐”为目标,而不是简单选择卸载层数最多的组合。
|
||||
|
||||
## 7.3 最终结论
|
||||
|
||||
本项目在 vLLM Ascend v0.18.0 上完成了 `offload_group_size` 与 `offload_num_in_group` 的参数适配、Ascend 分组预取卸载和完整复现链。修复后的 `qwen-chat-v1` 协议下,1.5B 三组 GSM8K 均为 72.2517%、GPQA Diamond 均为 23.7374%,两个 offload case 的额外精度下降为 0,且逐题输出哈希一致。
|
||||
|
||||
独立的 7B 证据表明,`(28,1)` 在当前负载下释放约 0.434 GiB 权重容量、将 KV cache 提升至 16.09 GiB,并保留约 85.77% 吞吐,是实际容量膝点。current/latest 同运行时与长窗口证据均未确认稳定速度优势,因此本项目不宣称提速。
|
||||
|
||||
项目的主要价值是把“是否卸载、每组卸载多少层”转化为可配置、可测试、可复现且可审计的工程能力。应在 HBM 容量成为主要约束时选择合适参数;吞吐优先且模型可直接装入时保持 baseline,并在目标负载上重新验证。
|
||||
|
||||
---
|
||||
|
||||
# 附录 A:完整复现
|
||||
|
||||
## A.1 环境安装
|
||||
|
||||
首先安装 CANN 8.5.1 toolkit、910B operator package 和 NNAL,然后执行:
|
||||
|
||||
```bash
|
||||
cd /data/vllm-ascend-v0.18.0
|
||||
bash scripts/install_v018_runtime_compat.sh
|
||||
```
|
||||
|
||||
## A.2 设备配置和健康检查
|
||||
|
||||
```bash
|
||||
export ASCEND_RT_VISIBLE_DEVICES=0
|
||||
export NPU_INDEX=0
|
||||
export NPU_CHIP_INDEX=0
|
||||
unset SKIP_NPU_HEALTH_CHECK
|
||||
bash scripts/check_npu_health.sh
|
||||
```
|
||||
|
||||
预期包含:
|
||||
|
||||
```text
|
||||
Health Status : OK
|
||||
Error Code : NA
|
||||
Error Information : NA
|
||||
```
|
||||
|
||||
## A.3 功能测试
|
||||
|
||||
```bash
|
||||
cd /data/vllm-ascend-v0.18.0
|
||||
python -m pytest tests/model_executor/offloader -q
|
||||
```
|
||||
|
||||
正式记录:
|
||||
|
||||
```text
|
||||
78 passed in 11.77s
|
||||
```
|
||||
|
||||
## A.4 性能复现
|
||||
|
||||
```bash
|
||||
cd /data/vllm-ascend-v0.18.0
|
||||
MODEL_PATH=/data/models/Qwen2.5-1.5B-Instruct \
|
||||
RESULTS_DIR=/data/v018-performance-results \
|
||||
REPEATS=3 \
|
||||
WARMUP_ROUNDS=1 \
|
||||
MEASURED_ROUNDS=3 \
|
||||
PROMPT_COUNT=8 \
|
||||
INPUT_TOKENS=128 \
|
||||
MAX_TOKENS=64 \
|
||||
MAX_MODEL_LEN=512 \
|
||||
bash scripts/run_benchmark_all.sh
|
||||
```
|
||||
|
||||
产物:
|
||||
|
||||
```text
|
||||
performance-runs.jsonl
|
||||
performance-summary.json
|
||||
performance-summary.csv
|
||||
performance-summary.md
|
||||
performance-run.log
|
||||
```
|
||||
|
||||
## A.5 精度复现
|
||||
|
||||
```bash
|
||||
cd /data/vllm-ascend-v0.18.0
|
||||
CANN_ENV=/usr/local/Ascend/cann-9.0.0/set_env.sh \
|
||||
VENV_ACTIVATE=/data/vllm-ascend-v0.18.0/.venv/bin/activate \
|
||||
MODEL_PATH=/data/models/Qwen2.5-1.5B-Instruct \
|
||||
DATA_DIR=/data/accuracy-gsm8k-gpqa \
|
||||
RESULTS_DIR=/data/v018-accuracy-results \
|
||||
ACCURACY_LIMIT=0 \
|
||||
GSM8K_MAX_TOKENS=512 \
|
||||
GPQA_MAX_TOKENS=8 \
|
||||
MAX_ACCURACY_DROP=0 \
|
||||
bash scripts/run_accuracy_gsm8k_gpqa.sh
|
||||
```
|
||||
|
||||
`ACCURACY_LIMIT=2` 仅用于管线冒烟,不得作为提交精度证据。
|
||||
|
||||
## A.6 一键复现
|
||||
|
||||
```bash
|
||||
cd /data/vllm-ascend-v0.18.0
|
||||
MODEL_PATH=/data/models/Qwen2.5-1.5B-Instruct \
|
||||
DATA_DIR=/data/accuracy-gsm8k-gpqa \
|
||||
REPRO_RESULTS_DIR=/data/v018-reproduction-results \
|
||||
bash scripts/run_reproduce_all.sh
|
||||
```
|
||||
|
||||
统一结果目录:
|
||||
|
||||
```text
|
||||
/data/v018-reproduction-results/
|
||||
reproduction-manifest.json
|
||||
performance/
|
||||
performance-runs.jsonl
|
||||
performance-summary.json
|
||||
performance-summary.csv
|
||||
performance-summary.md
|
||||
performance-run.log
|
||||
accuracy/
|
||||
baseline_0_1/
|
||||
group_8_num_1/
|
||||
group_8_num_2/
|
||||
accuracy-summary.json
|
||||
accuracy-summary.csv
|
||||
accuracy-summary.md
|
||||
```
|
||||
|
||||
## A.7 输出哈希复核
|
||||
|
||||
```bash
|
||||
sha256sum \
|
||||
/data/v018-accuracy-results/baseline_0_1/gsm8k-examples.jsonl \
|
||||
/data/v018-accuracy-results/group_8_num_1/gsm8k-examples.jsonl \
|
||||
/data/v018-accuracy-results/group_8_num_2/gsm8k-examples.jsonl
|
||||
|
||||
sha256sum \
|
||||
/data/v018-accuracy-results/baseline_0_1/gpqa-examples.jsonl \
|
||||
/data/v018-accuracy-results/group_8_num_1/gpqa-examples.jsonl \
|
||||
/data/v018-accuracy-results/group_8_num_2/gpqa-examples.jsonl
|
||||
```
|
||||
|
||||
每组数据集的三个哈希应完全相同。
|
||||
|
||||
## A.8 7B current/latest 对比与归档复核
|
||||
|
||||
在精确项目根执行前,三个 runner-owned 目录必须不存在:
|
||||
|
||||
```text
|
||||
/data/reproduction-results/qwen25-7b-current-vs-latest-20260731-rerun
|
||||
/data/reproduction-results/qwen25-7b-current-vs-latest-20260731-rerun.current_impl_overlay
|
||||
/data/reproduction-results/qwen25-7b-current-vs-latest-20260731-rerun.cache
|
||||
```
|
||||
|
||||
随后执行精确对比命令:
|
||||
|
||||
```bash
|
||||
cd /data/ccf-vllm-ascend-ec673f8-exact
|
||||
RESULTS_DIR=/data/reproduction-results/qwen25-7b-current-vs-latest-20260731-rerun \
|
||||
bash /data/ccf-vllm-ascend-ec673f8-exact/scripts/run_large_model_parameter_comparison.sh
|
||||
```
|
||||
|
||||
产物目录包含 `same-runtime-runs.jsonl`、`long-window-runs.jsonl`、`native-v018-runs.jsonl`、对应 `*-logs/`、runtime evidence sidecar、`comparison-summary.json`、`comparison-summary.csv`、`comparison-summary.md`、`validation.json` 和 `independent-verification.log`。复核 `validation.json` 的 `valid=true`、`model_files_verified=true`,确认 63 + 15 + 12 = 90 条记录与日志计数,再检查独立重算的三个 `MAX_*_DIFF` 均为 0。
|
||||
|
||||
最终归档为 `qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8-results.tar.gz`,SHA-256:
|
||||
|
||||
```text
|
||||
a8cc0ae4aa506d5c75f97d891001006889554e6b4b14603758e0a5e2014521f3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 附录 B:Qwen2.5-7B 固定负载参数建议
|
||||
|
||||
下表仅适用于本报告的单卡 Qwen2.5-7B 固定负载:8 个 prompts、128 输入 tokens、64 最大输出 tokens、`max_model_len=512` 和 `gpu_memory_utilization=0.5`。它描述容量与吞吐边界,不表示当前实现相对 official latest 存在速度优势。
|
||||
|
||||
| 场景 | 建议 |
|
||||
| --- | --- |
|
||||
| 模型和 KV cache 均可直接装入,吞吐优先 | 保持 `baseline`;可用 KV cache 为 15.66 GiB,作为 100% 吞吐参考。 |
|
||||
| 需要约 0.434 GiB 额外容量,且可接受约 14.23% 吞吐代价 | 优先评估 `(28,1)`;可用 KV cache 为 16.09 GiB,吞吐保留约 85.77%,是本负载的实际容量膝点。 |
|
||||
| baseline 和 `(28,1)` 均无法满足容量约束 | 在容量优先条件下评估 `(14,1)`;可用 KV cache 为 16.52 GiB、额外释放约 0.868 GiB 权重容量,但吞吐仅保留约 44.68%。 |
|
||||
| 多卡、长上下文、高并发或不同模型 | 不直接套用本表数字;必须在目标负载重新压测,并且不将微小 current/latest 差异解释为稳定提速。 |
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
# `offload_group_size` NPU 适配实现计划
|
||||
|
||||
> 赛道: CCF 开源创新大赛 vLLM Ascend 参数适配专项赛
|
||||
> 参数: `offload_group_size`
|
||||
> 状态: 已实现并通过单测、冒烟与 1.5B 参数矩阵测试
|
||||
> 日期: 2026-07-06
|
||||
|
||||
## 一、需求重述
|
||||
|
||||
`offload_group_size` 控制 decoder layer 的分组粒度。vLLM Ascend 需要在模型加载阶段根据该参数划分 layer group,并与 `offload_num_in_group` 配合决定哪些 layer 使用权重 offload。
|
||||
|
||||
默认值必须保持兼容:
|
||||
|
||||
- `offload_group_size <= 0`: 不启用权重 offload,返回 `NoopOffloader`
|
||||
- `offload_group_size > 0`: 进入 Ascend offloader 创建流程
|
||||
- 若 `offload_num_in_group > offload_group_size`: 启动阶段直接报错
|
||||
|
||||
## 二、参数语义
|
||||
|
||||
对 decoder layers 使用 0-based index 分组:
|
||||
|
||||
```text
|
||||
group_id = layer_id // offload_group_size
|
||||
offset = layer_id % offload_group_size
|
||||
```
|
||||
|
||||
在 `offload_num_in_group=1` 时,`offload_group_size` 越小,每组越短,被选中的 layer 越密集。
|
||||
|
||||
示例:
|
||||
|
||||
| num_layers | offload_group_size | offload_num_in_group | selected layers |
|
||||
|---:|---:|---:|---|
|
||||
| 8 | 0 | 1 | `()` |
|
||||
| 8 | 1 | 1 | `(0, 1, 2, 3, 4, 5, 6, 7)` |
|
||||
| 8 | 2 | 1 | `(1, 3, 5, 7)` |
|
||||
| 8 | 4 | 1 | `(3, 7)` |
|
||||
|
||||
## 三、实现方案
|
||||
|
||||
`offload_group_size` 的核心实现集中在:
|
||||
|
||||
- `vllm_ascend/model_executor/offloader/selection.py`
|
||||
- `vllm_ascend/worker/model_runner_v1.py`
|
||||
|
||||
`selection.py` 中使用纯函数实现参数语义:
|
||||
|
||||
```python
|
||||
select_offload_layers(
|
||||
num_layers=num_layers,
|
||||
group_size=offload_group_size,
|
||||
num_in_group=offload_num_in_group,
|
||||
)
|
||||
```
|
||||
|
||||
`model_runner_v1.py` 中通过 `create_ascend_weight_offloader()` 读取配置:
|
||||
|
||||
```python
|
||||
group_size = prefetch_config.offload_group_size
|
||||
num_in_group = prefetch_config.offload_num_in_group
|
||||
```
|
||||
|
||||
当 `group_size <= 0` 时返回 `NoopOffloader`,确保默认行为不变。
|
||||
|
||||
## 四、涉及文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `vllm_ascend/model_executor/offloader/selection.py` | 实现 group size 对 layer 分组的影响 |
|
||||
| `vllm_ascend/model_executor/offloader/sync.py` | 同步 fallback 复用选择结果 |
|
||||
| `vllm_ascend/model_executor/offloader/prefetch.py` | 预取 offloader 复用选择结果 |
|
||||
| `vllm_ascend/worker/model_runner_v1.py` | 读取参数并创建 Ascend offloader |
|
||||
| `tests/model_executor/offloader/offload_group_size/test_param.py` | `offload_group_size` 参数级单测 |
|
||||
|
||||
## 五、兼容性
|
||||
|
||||
- 默认 `offload_group_size=0` 不改变现有模型加载和推理行为。
|
||||
- 参数逻辑不依赖真实 NPU,可通过无设备单测验证。
|
||||
- 实际 offload 执行仍由 sync/prefetch 后端负责,`offload_group_size` 只决定 layer 分组密度。
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# `offload_group_size` 测试报告
|
||||
|
||||
> 状态: 通过
|
||||
> 测试日期: 2026-07-05
|
||||
|
||||
## 1. 测试范围
|
||||
|
||||
- `offload_group_size=0` 保持 Noop
|
||||
- `offload_group_size=1/2/4` 对 selected layers 的影响
|
||||
- 正数 group size 可创建 Ascend prefetch offloader
|
||||
- 与共享 selection、sync、prefetch、runner 集成测试共同覆盖参数链路
|
||||
|
||||
## 2. 单测结果
|
||||
|
||||
完整 offloader 测试集结果:
|
||||
|
||||
```text
|
||||
35 passed, 3 warnings in 12.71s
|
||||
```
|
||||
|
||||
## 3. 参数矩阵实测
|
||||
|
||||
固定 `offload_num_in_group=1` 时,`offload_group_size` 越小,offload layer 越多,HBM 节省越高但短请求吞吐下降更明显。
|
||||
|
||||
| case | group_size | num_in_group | offload layers | saved GB | output tok/s |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| baseline_0_1 | 0 | 1 | 0 | 0.0000 | 38.8511 |
|
||||
| group_8_num_1 | 8 | 1 | 3 | 0.2615 | 41.7467 |
|
||||
| group_4_num_1 | 4 | 1 | 7 | 0.6102 | 24.5113 |
|
||||
| group_2_num_1 | 2 | 1 | 14 | 1.2203 | 14.5874 |
|
||||
|
||||
## 4. 结论
|
||||
|
||||
`offload_group_size` 已按预期控制 layer 分组粒度。该参数主要用于调节 HBM 节省与吞吐损失之间的平衡。
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
# `offload_group_size` 单元测试实施方案
|
||||
|
||||
> 测试目录: `tests/model_executor/offloader/offload_group_size`
|
||||
|
||||
## 一、测试目标
|
||||
|
||||
验证 `offload_group_size` 对 decoder layer 分组粒度的影响,并确保默认值保持 Noop。
|
||||
|
||||
## 二、测试文件
|
||||
|
||||
```text
|
||||
tests/model_executor/offloader/offload_group_size/test_param.py
|
||||
```
|
||||
|
||||
## 三、测试矩阵
|
||||
|
||||
| 用例 | 输入 | 预期 |
|
||||
|---|---|---|
|
||||
| disabled | `group_size=0, num_in_group=1` | `layer_indices == ()` |
|
||||
| every layer | `group_size=1, num_in_group=1` | 选择所有 layer |
|
||||
| dense grouping | `group_size=2, num_in_group=1` | `(1, 3, 5, 7)` |
|
||||
| sparse grouping | `group_size=4, num_in_group=1` | `(3, 7)` |
|
||||
| default Noop | `create_ascend_weight_offloader(make_config(0, 1))` | `NoopOffloader` |
|
||||
| enabled backend | `create_ascend_weight_offloader(make_config(4, 1))` | `AscendPrefetchOffloader` |
|
||||
|
||||
## 四、运行方式
|
||||
|
||||
```bash
|
||||
PYTHONPATH=/data/vllm-ascend-dev/worktree \
|
||||
/data/vllm-ascend-smoke/venv/bin/python -m pytest \
|
||||
tests/model_executor/offloader/offload_group_size -q
|
||||
```
|
||||
|
||||
## 五、测试原则
|
||||
|
||||
`offload_group_size` 的测试必须断言具体 layer tuple,不能只断言数量。否则无法发现分组方向或 offset 计算错误。
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# `offload_num_in_group` NPU 适配实现计划
|
||||
|
||||
> 赛道: CCF 开源创新大赛 vLLM Ascend 参数适配专项赛
|
||||
> 参数: `offload_num_in_group`
|
||||
> 状态: 已实现并通过单测、冒烟与 1.5B 参数矩阵测试
|
||||
> 日期: 2026-07-06
|
||||
|
||||
## 一、需求重述
|
||||
|
||||
`offload_num_in_group` 控制每个 decoder layer group 中有多少层进行权重 offload。它必须与 `offload_group_size` 配合使用,并满足:
|
||||
|
||||
```text
|
||||
0 <= offload_num_in_group <= offload_group_size
|
||||
```
|
||||
|
||||
当 `offload_num_in_group=0` 时,即使 `offload_group_size` 为正数,也必须保持 Noop。
|
||||
|
||||
## 二、参数语义
|
||||
|
||||
本实现选择每个 group 的末尾 N 层进行 offload:
|
||||
|
||||
```text
|
||||
offset = layer_id % offload_group_size
|
||||
offload if offset >= offload_group_size - offload_num_in_group
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
| num_layers | offload_group_size | offload_num_in_group | selected layers |
|
||||
|---:|---:|---:|---|
|
||||
| 12 | 4 | 0 | `()` |
|
||||
| 12 | 4 | 1 | `(3, 7, 11)` |
|
||||
| 12 | 4 | 2 | `(2, 3, 6, 7, 10, 11)` |
|
||||
| 12 | 4 | 4 | `(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11)` |
|
||||
|
||||
## 三、实现方案
|
||||
|
||||
`offload_num_in_group` 的校验和选择逻辑在 `selection.py` 中统一完成:
|
||||
|
||||
```python
|
||||
if num_in_group < 0:
|
||||
raise ValueError("offload_num_in_group must be >= 0")
|
||||
if num_in_group > group_size:
|
||||
raise ValueError(...)
|
||||
```
|
||||
|
||||
`model_runner_v1.py` 在创建 offloader 前也做启动阶段校验,确保非法配置尽早失败:
|
||||
|
||||
```python
|
||||
if num_in_group > group_size:
|
||||
raise ValueError(...)
|
||||
```
|
||||
|
||||
## 四、涉及文件
|
||||
|
||||
| 文件 | 说明 |
|
||||
|---|---|
|
||||
| `vllm_ascend/model_executor/offloader/selection.py` | 实现每组 offload 层数选择与非法参数校验 |
|
||||
| `vllm_ascend/model_executor/offloader/sync.py` | 同步 fallback 使用该参数选择 offload modules |
|
||||
| `vllm_ascend/model_executor/offloader/prefetch.py` | 预取实现使用该参数选择 offload modules |
|
||||
| `vllm_ascend/worker/model_runner_v1.py` | 读取参数并进行启动阶段校验 |
|
||||
| `tests/model_executor/offloader/offload_num_in_group/test_param.py` | `offload_num_in_group` 参数级单测 |
|
||||
|
||||
## 五、兼容性
|
||||
|
||||
- `offload_num_in_group=0` 不启用 offload。
|
||||
- `offload_num_in_group > offload_group_size` 明确报错,避免模糊行为。
|
||||
- 参数只影响每组内 offload 层数,不改变 sync/prefetch 后端的执行接口。
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
# `offload_num_in_group` 测试报告
|
||||
|
||||
> 状态: 通过
|
||||
> 测试日期: 2026-07-05
|
||||
|
||||
## 1. 测试范围
|
||||
|
||||
- `offload_num_in_group=0` 保持 Noop
|
||||
- `offload_num_in_group=1/2/4` 对每组 selected layers 的影响
|
||||
- `offload_num_in_group > offload_group_size` 启动阶段报错
|
||||
- 与共享 selection、sync、prefetch、runner 集成测试共同覆盖参数链路
|
||||
|
||||
## 2. 单测结果
|
||||
|
||||
完整 offloader 测试集结果:
|
||||
|
||||
```text
|
||||
35 passed, 3 warnings in 12.71s
|
||||
```
|
||||
|
||||
## 3. 参数矩阵实测
|
||||
|
||||
固定 `offload_group_size=4` 时,`offload_num_in_group` 增大后每组 offload 层数增加,HBM 节省增大,吞吐下降。
|
||||
|
||||
| case | group_size | num_in_group | offload layers | saved GB | output tok/s |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| baseline_0_1 | 0 | 1 | 0 | 0.0000 | 38.8511 |
|
||||
| group_4_num_1 | 4 | 1 | 7 | 0.6102 | 24.5113 |
|
||||
| group_4_num_2 | 4 | 2 | 14 | 1.2203 | 14.9576 |
|
||||
|
||||
非法参数验证:
|
||||
|
||||
```text
|
||||
offload_num_in_group > offload_group_size -> SMOKE_INVALID_OK
|
||||
```
|
||||
|
||||
## 4. 结论
|
||||
|
||||
`offload_num_in_group` 已按预期控制每个 group 内 offload 层数,并能拒绝非法参数组合。
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# `offload_num_in_group` 单元测试实施方案
|
||||
|
||||
> 测试目录: `tests/model_executor/offloader/offload_num_in_group`
|
||||
|
||||
## 一、测试目标
|
||||
|
||||
验证 `offload_num_in_group` 对每个 group 内 offload 层数的影响,并确保非法组合能被启动阶段拒绝。
|
||||
|
||||
## 二、测试文件
|
||||
|
||||
```text
|
||||
tests/model_executor/offloader/offload_num_in_group/test_param.py
|
||||
```
|
||||
|
||||
## 三、测试矩阵
|
||||
|
||||
| 用例 | 输入 | 预期 |
|
||||
|---|---|---|
|
||||
| disabled | `group_size=4, num_in_group=0` | `layer_indices == ()` |
|
||||
| one per group | `group_size=4, num_in_group=1` | `(3, 7, 11)` |
|
||||
| two per group | `group_size=4, num_in_group=2` | `(2, 3, 6, 7, 10, 11)` |
|
||||
| all in group | `group_size=4, num_in_group=4` | 选择所有 layer |
|
||||
| default Noop | `create_ascend_weight_offloader(make_config(4, 0))` | `NoopOffloader` |
|
||||
| invalid M>N | `create_ascend_weight_offloader(make_config(2, 3))` | `ValueError` |
|
||||
| valid backend | `create_ascend_weight_offloader(make_config(4, 2))` | `AscendPrefetchOffloader` |
|
||||
|
||||
## 四、运行方式
|
||||
|
||||
```bash
|
||||
PYTHONPATH=/data/vllm-ascend-dev/worktree \
|
||||
/data/vllm-ascend-smoke/venv/bin/python -m pytest \
|
||||
tests/model_executor/offloader/offload_num_in_group -q
|
||||
```
|
||||
|
||||
## 五、测试原则
|
||||
|
||||
`offload_num_in_group` 的测试应固定 `offload_group_size`,只改变 `num_in_group`,避免两个参数的影响混在一起。
|
||||
|
|
@ -0,0 +1,261 @@
|
|||
# 决赛材料制作 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 基于当前优化分支、初赛结项书、复现脚本和已有测试证据,生成放置于 `docs/finals/` 的决赛结项书 Markdown、Word 版本及 12 分钟答辩 PPT。
|
||||
|
||||
**Architecture:** 先建立单一事实清单,再由该清单驱动 Markdown、DOCX 和 PPT,避免三个交付物出现数字漂移。Word 采用正式技术报告样式并进行逐页渲染检查;PPT 继承赛事模板母版和布局,使用 Artifact Tool 编辑并进行逐页渲染、内容溢出和模板一致性检查。
|
||||
|
||||
**Tech Stack:** Markdown、Git、Python `python-docx`/OOXML、LibreOffice、`@oai/artifact-tool`、PowerPoint OOXML、Node.js、赛事 PPT 模板。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 主标题必须为 `[功能] 适配 offload_group_size 和 offload_num_in_group 到 Ascend NPU`。
|
||||
- 副标题必须为 `vLLM Ascend 权重分组预取卸载参数适配与优化`。
|
||||
- 团队名称必须为 `乘风破浪的大学生`。
|
||||
- 三份最终交付物必须位于 `docs/finals/`。
|
||||
- PPT 必须基于 `D:\PDSL\Ascend\CCF大赛-vLLM Ascend赛道PPT模板.pptx`,保持 12 页并适配 12 分钟答辩。
|
||||
- 所有测试数字必须来自仓库现有证据;GPQA 0/198 必须如实说明,不得包装为高精度结果。
|
||||
- 不修改核心实现代码和已有测试结果。
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
- Create: `docs/finals/决赛项目结项书.md` - 决赛结项书的内容权威版本。
|
||||
- Create: `docs/finals/决赛项目结项书.docx` - 正式排版版本。
|
||||
- Create: `docs/finals/决赛答辩PPT.pptx` - 12 页决赛答辩材料。
|
||||
- Create: `.codex_tmp/finals-materials/evidence-summary.md` - 制作过程使用的事实清单。
|
||||
- Create: `.codex_tmp/finals-materials/build_final_report.py` - DOCX 生成脚本。
|
||||
- Create: `.codex_tmp/finals-materials/build_final_deck.mjs` - PPT 生成脚本。
|
||||
- Create: `.codex_tmp/finals-materials/template-audit.txt` - 模板逐页审计记录。
|
||||
- Create: `.codex_tmp/finals-materials/template-frame-map.json` - 输出页与模板页映射。
|
||||
- Create: `.codex_tmp/finals-materials/deviation-log.txt` - 模板偏差记录。
|
||||
- Create: `.codex_tmp/finals-materials/docx-render/` - Word 逐页渲染结果。
|
||||
- Create: `.codex_tmp/finals-materials/ppt-render/` - PPT 逐页渲染结果。
|
||||
|
||||
### Task 1: 建立统一事实清单
|
||||
|
||||
**Files:**
|
||||
- Create: `.codex_tmp/finals-materials/evidence-summary.md`
|
||||
- Read: `docs/final-report.md`
|
||||
- Read: `docs/v018-deployment-and-reproduction.md`
|
||||
- Read: `docs/v018-test-evidence.md`
|
||||
- Read: `docs/benchmark/offload-benchmark-report.md`
|
||||
- Read: `scripts/run_reproduce_all.sh`
|
||||
- Read: `scripts/run_accuracy_gsm8k_gpqa.sh`
|
||||
- Read: `scripts/verify_accuracy_matrix.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 当前分支代码、提交历史及仓库内测试证据。
|
||||
- Produces: 三份交付物共同使用的版本、实现、精度、性能和复现事实。
|
||||
|
||||
- [ ] **Step 1: 提取版本、实现、性能、精度和复现命令**
|
||||
|
||||
逐文件核对并记录数值、路径和限定条件;对重复数字进行交叉检查。
|
||||
|
||||
- [ ] **Step 2: 检查代码实现与文档描述一致性**
|
||||
|
||||
使用 `rg` 定位 `offload_group_size`、`offload_num_in_group`、`NPUPrefetchOffloader`、`offload_backend`、pinned storage 和设备映射实现。
|
||||
|
||||
- [ ] **Step 3: 写入事实清单并扫描矛盾**
|
||||
|
||||
人工核对事实清单中是否存在未完成段落、模糊数值或互相冲突的环境描述。
|
||||
|
||||
Expected: 不出现占位符;近似数值均明确单位和来源。
|
||||
|
||||
### Task 2: 编写决赛结项书 Markdown
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/finals/决赛项目结项书.md`
|
||||
- Read: `.codex_tmp/finals-materials/evidence-summary.md`
|
||||
- Read: `docs/final-report.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 的事实清单和初赛七章结构。
|
||||
- Produces: Word 与 PPT 文案的内容权威版本。
|
||||
|
||||
- [ ] **Step 1: 按七章结构撰写正文**
|
||||
|
||||
覆盖项目概述、参数难点、技术方案、实际交付、测试结论、AI 使用和限制及后续工作。
|
||||
|
||||
- [ ] **Step 2: 补充复现步骤和判定标准**
|
||||
|
||||
包含环境准备、功能测试、性能测试、GSM8K、GPQA、结果校验和产物路径。
|
||||
|
||||
- [ ] **Step 3: 完成数字和路径一致性检查**
|
||||
|
||||
Run: `rg -n "742\\.77|678\\.85|383\\.49|51/1319|0/198|35/35|69 passed" docs/finals/决赛项目结项书.md`
|
||||
|
||||
Expected: 所有关键数据均出现,且上下文描述与事实清单一致。
|
||||
|
||||
- [ ] **Step 4: 检查 Markdown 质量**
|
||||
|
||||
Run: `git diff --check -- docs/finals/决赛项目结项书.md`
|
||||
|
||||
Expected: 无空白错误或冲突标记。
|
||||
|
||||
### Task 3: 生成并验证 Word 结项书
|
||||
|
||||
**Files:**
|
||||
- Create: `.codex_tmp/finals-materials/build_final_report.py`
|
||||
- Create: `docs/finals/决赛项目结项书.docx`
|
||||
- Create: `.codex_tmp/finals-materials/docx-render/`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `docs/finals/决赛项目结项书.md`。
|
||||
- Produces: 内容一致、可打印、经过逐页检查的 DOCX。
|
||||
|
||||
- [ ] **Step 1: 读取文档设计规范并确定样式令牌**
|
||||
|
||||
采用 formal technical report / standard business brief:A4 纵向、中文无衬线正文、蓝色标题体系、封面、自动目录字段、页眉、页脚和页码。
|
||||
|
||||
- [ ] **Step 2: 编写 DOCX 生成脚本**
|
||||
|
||||
将标题、段落、真实项目符号、代码块和数据表转换为 Word 样式;表格固定列宽并避免跨页断裂。
|
||||
|
||||
- [ ] **Step 3: 生成 Word 文档**
|
||||
|
||||
Run: `python .codex_tmp/finals-materials/build_final_report.py`
|
||||
|
||||
Expected: `docs/finals/决赛项目结项书.docx` 可被 OOXML 工具正常读取。
|
||||
|
||||
- [ ] **Step 4: 运行结构和可访问性检查**
|
||||
|
||||
Run: `python <documents-skill>/scripts/a11y_audit.py docs/finals/决赛项目结项书.docx`
|
||||
|
||||
Expected: 无阻断级错误。
|
||||
|
||||
- [ ] **Step 5: 渲染全部页面**
|
||||
|
||||
Run: `python <documents-skill>/render_docx.py docs/finals/决赛项目结项书.docx --output_dir .codex_tmp/finals-materials/docx-render --emit_pdf`
|
||||
|
||||
Expected: 每页生成 PNG,无转换错误。
|
||||
|
||||
- [ ] **Step 6: 逐页视觉检查并迭代**
|
||||
|
||||
检查封面、目录、标题孤行、表格截断、代码换行、页眉页脚和中文字体;发现问题后修改脚本、重建并重新渲染。
|
||||
|
||||
### Task 4: 审计赛事 PPT 模板并建立页面映射
|
||||
|
||||
**Files:**
|
||||
- Create: `.codex_tmp/finals-materials/template-audit.txt`
|
||||
- Create: `.codex_tmp/finals-materials/template-frame-map.json`
|
||||
- Create: `.codex_tmp/finals-materials/deviation-log.txt`
|
||||
- Read: `D:\PDSL\Ascend\CCF大赛-vLLM Ascend赛道PPT模板.pptx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 赛事模板的 12 个源页面、母版和布局。
|
||||
- Produces: PPT 构建脚本的页面继承与可编辑区域约束。
|
||||
|
||||
- [ ] **Step 1: 检查 12 个模板页及全部布局元素**
|
||||
|
||||
记录各页用途、固定品牌元素、可替换元素、提示文字和可用内容区域。
|
||||
|
||||
- [ ] **Step 2: 建立一一页面映射**
|
||||
|
||||
将 12 个输出页面分别映射至模板 1-12 页,不新增或删除页。
|
||||
|
||||
- [ ] **Step 3: 记录必要偏差**
|
||||
|
||||
仅记录清除示意提示、替换占位内容和在模板正文区域中加入图表等必要变化。
|
||||
|
||||
### Task 5: 制作 12 分钟决赛答辩 PPT
|
||||
|
||||
**Files:**
|
||||
- Create: `.codex_tmp/finals-materials/build_final_deck.mjs`
|
||||
- Create: `docs/finals/决赛答辩PPT.pptx`
|
||||
- Read: `docs/finals/决赛项目结项书.md`
|
||||
- Read: `.codex_tmp/finals-materials/template-frame-map.json`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2 文案、Task 4 模板映射和原始 PPTX。
|
||||
- Produces: 12 页、含讲述备注和来源备注的答辩 PPT。
|
||||
|
||||
- [ ] **Step 1: 创建模板继承的工作副本**
|
||||
|
||||
使用 presentations skill 的模板辅助脚本准备 starter deck,保持母版和布局层级。
|
||||
|
||||
- [ ] **Step 2: 编写 Artifact Tool 构建脚本**
|
||||
|
||||
逐页编辑继承元素,加入参数关系图、实现数据流、验证矩阵、精度对比和性能/显存权衡图;所有新增元素必须位于映射内容区内。
|
||||
|
||||
- [ ] **Step 3: 写入 12 分钟讲述备注**
|
||||
|
||||
每页记录建议时长、核心讲述句和证据来源;总讲述目标为 11 分 20 秒。
|
||||
|
||||
- [ ] **Step 4: 导出 PPTX**
|
||||
|
||||
Run: `node .codex_tmp/finals-materials/build_final_deck.mjs`
|
||||
|
||||
Expected: `docs/finals/决赛答辩PPT.pptx` 包含 12 页并继承模板主题。
|
||||
|
||||
### Task 6: PPT 渲染与模板一致性检查
|
||||
|
||||
**Files:**
|
||||
- Create: `.codex_tmp/finals-materials/ppt-render/`
|
||||
- Modify: `.codex_tmp/finals-materials/build_final_deck.mjs`
|
||||
- Modify: `docs/finals/决赛答辩PPT.pptx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 5 输出的 PPTX。
|
||||
- Produces: 无溢出、无遮挡、模板一致性合格的最终 PPTX。
|
||||
|
||||
- [ ] **Step 1: 渲染全部幻灯片**
|
||||
|
||||
Run: `python <presentations-skill>/render_slides.py docs/finals/决赛答辩PPT.pptx --output_dir .codex_tmp/finals-materials/ppt-render`
|
||||
|
||||
Expected: 12 张 PNG 全部生成。
|
||||
|
||||
- [ ] **Step 2: 逐页视觉检查**
|
||||
|
||||
检查品牌元素、标题层级、图表标签、中文换行、页码、备注和示意提示是否清除。
|
||||
|
||||
- [ ] **Step 3: 运行幻灯片结构检查**
|
||||
|
||||
Run: `python <presentations-skill>/slides_test.py docs/finals/决赛答辩PPT.pptx`
|
||||
|
||||
Expected: 无文本溢出、边界越界或损坏对象错误。
|
||||
|
||||
- [ ] **Step 4: 运行模板一致性检查**
|
||||
|
||||
比较页面数、母版、布局关系和关键固定元素;将可接受偏差写入 `deviation-log.txt`。
|
||||
|
||||
- [ ] **Step 5: 修复问题并重新执行全部检查**
|
||||
|
||||
对任何截断、重叠、密度过高或模板偏移进行迭代,直到所有 12 页检查通过。
|
||||
|
||||
### Task 7: 最终一致性与仓库验收
|
||||
|
||||
**Files:**
|
||||
- Verify: `docs/finals/决赛项目结项书.md`
|
||||
- Verify: `docs/finals/决赛项目结项书.docx`
|
||||
- Verify: `docs/finals/决赛答辩PPT.pptx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2、3、6 的最终输出。
|
||||
- Produces: 可提交的决赛材料目录。
|
||||
|
||||
- [ ] **Step 1: 核对三个文件存在且非空**
|
||||
|
||||
Run: `Get-Item docs/finals/* | Select-Object Name,Length`
|
||||
|
||||
Expected: 三个目标文件均存在且大小大于 0。
|
||||
|
||||
- [ ] **Step 2: 跨文件核对标题、团队、版本和数字**
|
||||
|
||||
从 Markdown、DOCX 和 PPTX 中提取文本,比对主标题、团队名称、版本号、精度和性能关键数字。
|
||||
|
||||
- [ ] **Step 3: 检查 Git 工作区范围**
|
||||
|
||||
Run: `git status --short`
|
||||
|
||||
Expected: 仅包含计划内文档、交付物和必要制作记录,不包含核心代码变化。
|
||||
|
||||
- [ ] **Step 4: 提交最终材料**
|
||||
|
||||
Run: `git add docs/finals docs/superpowers/plans/2026-07-29-finals-materials.md`
|
||||
|
||||
Run: `git commit -m "docs: add finals report and defense deck"`
|
||||
|
||||
Expected: 提交成功,工作区干净。
|
||||
|
|
@ -0,0 +1,445 @@
|
|||
# Accuracy Protocol Fix Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the truncating raw-text accuracy runner with one reproducible Qwen chat protocol that produces meaningful GSM8K and GPQA scores.
|
||||
|
||||
**Architecture:** Keep dataset loading and offload matrix orchestration unchanged. Add dataset-specific prompt construction, render every prompt through the model tokenizer's chat template, return structured generation records, and make the matrix verifier reject protocol mismatches between cases.
|
||||
|
||||
**Tech Stack:** Python 3, `unittest`, vLLM offline `LLM.generate`, Qwen tokenizer chat template, Bash, JSONL.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Produce one formal score set only, named `qwen-chat-v1`.
|
||||
- Use `temperature=0` for every case.
|
||||
- Use 512 generated tokens for GSM8K and 8 generated tokens for GPQA.
|
||||
- Preserve the existing GSM8K and GPQA dataset hashes and seeded GPQA option order.
|
||||
- Apply exactly the same prompt and scoring protocol to baseline, `group_8_num_1`, and `group_8_num_2`.
|
||||
- Permit zero accuracy drop relative to baseline.
|
||||
- Add no new runtime dependency.
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify `scripts/accuracy_gsm8k_gpqa.py`: prompt protocol, chat rendering, structured generation records, parsing, and result metadata.
|
||||
- Modify `scripts/run_accuracy_gsm8k_gpqa.sh`: formal per-dataset generation budgets.
|
||||
- Modify `scripts/verify_accuracy_matrix.py`: cross-case protocol and sampling consistency checks.
|
||||
- Modify `tests/model_executor/offloader/test_accuracy_repro.py`: all regression and matrix tests.
|
||||
- Modify `docs/v018-deployment-and-reproduction.md`: corrected protocol and final measured scores after the server run.
|
||||
- Modify `docs/v018-test-evidence.md`: corrected accuracy evidence after the server run.
|
||||
- Modify `docs/finals/决赛项目结项书.md`: replace obsolete 128/16-token score claims with the measured corrected matrix.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Dataset Prompts And GPQA Parsing
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/model_executor/offloader/test_accuracy_repro.py`
|
||||
- Modify: `scripts/accuracy_gsm8k_gpqa.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `PROMPT_PROTOCOL = "qwen-chat-v1"`.
|
||||
- Produces: `chat_messages_for_prompt(dataset: str, prompt: str) -> list[dict[str, str]]`.
|
||||
- Changes: `prompt_for_example(dataset, row, seed) -> tuple[str, str]` to include a dataset-specific answer instruction.
|
||||
- Changes: `parse_gpqa_answer(text) -> str | None` to parse only an explicit leading answer.
|
||||
|
||||
- [ ] **Step 1: Write failing prompt and parser tests**
|
||||
|
||||
Add tests with hand-derived expectations:
|
||||
|
||||
```python
|
||||
def test_formal_prompts_define_unambiguous_answer_formats(self):
|
||||
accuracy = load_accuracy_module()
|
||||
gsm_prompt, _ = accuracy.prompt_for_example(
|
||||
"gsm8k", {"question": "What is 2 + 3?", "answer": "#### 5"}, "0"
|
||||
)
|
||||
self.assertIn("The answer is <number>.", gsm_prompt)
|
||||
self.assertIn("What is 2 + 3?", gsm_prompt)
|
||||
|
||||
gpqa_row = {
|
||||
"Question": "Which value is correct?",
|
||||
"Correct Answer": "correct",
|
||||
"Incorrect Answer 1": "wrong one",
|
||||
"Incorrect Answer 2": "wrong two",
|
||||
"Incorrect Answer 3": "wrong three",
|
||||
}
|
||||
gpqa_prompt, _ = accuracy.prompt_for_example("gpqa", gpqa_row, "0")
|
||||
self.assertIn("Return only one capital letter", gpqa_prompt)
|
||||
self.assertTrue(gpqa_prompt.rstrip().endswith("Answer:"))
|
||||
|
||||
def test_chat_messages_use_dataset_specific_system_roles(self):
|
||||
accuracy = load_accuracy_module()
|
||||
messages = accuracy.chat_messages_for_prompt("gsm8k", "user prompt")
|
||||
self.assertEqual([message["role"] for message in messages], ["system", "user"])
|
||||
self.assertIn("math", messages[0]["content"].lower())
|
||||
self.assertEqual(messages[1]["content"], "user prompt")
|
||||
|
||||
def test_gpqa_parser_accepts_only_an_explicit_leading_choice(self):
|
||||
accuracy = load_accuracy_module()
|
||||
for text in ("C", "C. explanation", "Answer: C\nExplanation", "(c)"):
|
||||
self.assertEqual(accuracy.parse_gpqa_answer(text), "C")
|
||||
self.assertIsNone(accuracy.parse_gpqa_answer("I considered C, but chose B."))
|
||||
self.assertIsNone(accuracy.parse_gpqa_answer("Because C is plausible."))
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused tests and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python -m unittest discover \
|
||||
-s tests/model_executor/offloader \
|
||||
-p test_accuracy_repro.py
|
||||
```
|
||||
|
||||
Expected: failures because the prompts lack the new instructions,
|
||||
`chat_messages_for_prompt` does not exist, and explanatory GPQA answers are rejected.
|
||||
|
||||
- [ ] **Step 3: Implement the prompt protocol and anchored parser**
|
||||
|
||||
Add exact protocol constants and message construction:
|
||||
|
||||
```python
|
||||
PROMPT_PROTOCOL = "qwen-chat-v1"
|
||||
SYSTEM_MESSAGES = {
|
||||
"gsm8k": "You are a precise math problem solver. Follow the requested answer format.",
|
||||
"gpqa": "You are a precise expert multiple-choice question solver.",
|
||||
}
|
||||
|
||||
def chat_messages_for_prompt(dataset, prompt):
|
||||
return [
|
||||
{"role": "system", "content": SYSTEM_MESSAGES[dataset]},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
```
|
||||
|
||||
Build GSM8K prompts with a final-line answer instruction and GPQA prompts with
|
||||
an answer-only instruction. Replace the full-string GPQA regex with an anchored
|
||||
prefix regex using `match`, so later prose cannot supply the answer.
|
||||
|
||||
- [ ] **Step 4: Run the focused tests and verify GREEN**
|
||||
|
||||
Run the command from Step 2. Expected: all three tests pass.
|
||||
|
||||
- [ ] **Step 5: Run the complete accuracy unit test module**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python -m unittest discover \
|
||||
-s tests/model_executor/offloader \
|
||||
-p test_accuracy_repro.py
|
||||
```
|
||||
|
||||
Expected: all tests pass; any old standalone-only parser assertion is updated to
|
||||
the new explicit-leading-answer contract.
|
||||
|
||||
- [ ] **Step 6: Commit Task 1**
|
||||
|
||||
```bash
|
||||
git add scripts/accuracy_gsm8k_gpqa.py tests/model_executor/offloader/test_accuracy_repro.py
|
||||
git commit -m "fix: define formal accuracy prompts and parsing"
|
||||
```
|
||||
|
||||
### Task 2: Chat Rendering And Structured Generation Records
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/model_executor/offloader/test_accuracy_repro.py`
|
||||
- Modify: `scripts/accuracy_gsm8k_gpqa.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Changes: `generate_prompts(args, dataset, prompts) -> list[dict[str, object]]`.
|
||||
- Each generation record contains `text`, `output_token_count`, and `finish_reason`.
|
||||
- Produces: `render_chat_prompts(tokenizer, dataset, prompts) -> list[str]`.
|
||||
|
||||
- [ ] **Step 1: Write failing chat rendering and generation record tests**
|
||||
|
||||
Use a fake tokenizer only at the external tokenizer boundary:
|
||||
|
||||
```python
|
||||
class FakeTokenizer:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def apply_chat_template(
|
||||
self, messages, *, tokenize, add_generation_prompt
|
||||
):
|
||||
self.calls.append((messages, tokenize, add_generation_prompt))
|
||||
return "rendered:" + messages[-1]["content"]
|
||||
```
|
||||
|
||||
Make `FakeLLM.get_tokenizer()` return it and make `FakeLLM.generate()` return a
|
||||
complete vLLM-shaped candidate:
|
||||
|
||||
```python
|
||||
SimpleNamespace(
|
||||
outputs=[
|
||||
SimpleNamespace(
|
||||
text="The answer is 5.",
|
||||
token_ids=[10, 11, 12, 13],
|
||||
finish_reason="stop",
|
||||
)
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
Assert that the real function returns:
|
||||
|
||||
```python
|
||||
[{
|
||||
"text": "The answer is 5.",
|
||||
"output_token_count": 4,
|
||||
"finish_reason": "stop",
|
||||
}]
|
||||
```
|
||||
|
||||
and that `LLM.generate` receives `["rendered:prompt"]`.
|
||||
|
||||
- [ ] **Step 2: Run the focused generation test and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python -m unittest discover \
|
||||
-s tests/model_executor/offloader \
|
||||
-p test_accuracy_repro.py
|
||||
```
|
||||
|
||||
Expected: failure because `generate_prompts` has no dataset argument, does not
|
||||
render chat messages, and returns plain text.
|
||||
|
||||
- [ ] **Step 3: Implement chat rendering and generation records**
|
||||
|
||||
Instantiate `LLM` exactly once, call `llm.get_tokenizer()`, render every prompt
|
||||
with:
|
||||
|
||||
```python
|
||||
tokenizer.apply_chat_template(
|
||||
chat_messages_for_prompt(dataset, prompt),
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
```
|
||||
|
||||
Raise `RuntimeError("unable to apply chat template for <dataset>")` if rendering
|
||||
fails or does not return a non-empty string. Convert each vLLM request output
|
||||
into the three-field record. Use an empty record with finish reason `missing`
|
||||
when no candidate is returned.
|
||||
|
||||
- [ ] **Step 4: Run the focused test and verify GREEN**
|
||||
|
||||
Run the command from Step 2. Expected: pass.
|
||||
|
||||
- [ ] **Step 5: Run the complete accuracy unit test module**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python -m unittest discover \
|
||||
-s tests/model_executor/offloader \
|
||||
-p test_accuracy_repro.py
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 6: Commit Task 2**
|
||||
|
||||
```bash
|
||||
git add scripts/accuracy_gsm8k_gpqa.py tests/model_executor/offloader/test_accuracy_repro.py
|
||||
git commit -m "fix: apply model chat template during accuracy runs"
|
||||
```
|
||||
|
||||
### Task 3: Result Diagnostics, Formal Budgets, And Matrix Contracts
|
||||
|
||||
**Files:**
|
||||
- Modify: `tests/model_executor/offloader/test_accuracy_repro.py`
|
||||
- Modify: `scripts/accuracy_gsm8k_gpqa.py`
|
||||
- Modify: `scripts/run_accuracy_gsm8k_gpqa.sh`
|
||||
- Modify: `scripts/verify_accuracy_matrix.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `summarize_generation(records, max_tokens) -> dict[str, object]`.
|
||||
- Changes: `build_result_metadata(..., generation_summary)` to include
|
||||
`prompt_protocol` and `generation_summary`.
|
||||
- Requires every matrix result to share `prompt_protocol` and
|
||||
`sampling_settings` with its dataset baseline.
|
||||
|
||||
- [ ] **Step 1: Write failing result and verifier tests**
|
||||
|
||||
Add a generation summary test using literal records:
|
||||
|
||||
```python
|
||||
records = [
|
||||
{"text": "x", "output_token_count": 512, "finish_reason": "length"},
|
||||
{"text": "y", "output_token_count": 17, "finish_reason": "stop"},
|
||||
]
|
||||
self.assertEqual(
|
||||
accuracy.summarize_generation(records, 512),
|
||||
{
|
||||
"at_token_limit": 1,
|
||||
"finish_reasons": {"length": 1, "stop": 1},
|
||||
"output_tokens": 529,
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Update matrix fixtures to contain:
|
||||
|
||||
```python
|
||||
"prompt_protocol": {"name": "qwen-chat-v1", "chat_template": True},
|
||||
"sampling_settings": {"temperature": 0, "max_tokens": 512},
|
||||
```
|
||||
|
||||
Add a verifier test that changes one candidate protocol name to `raw-v0` and
|
||||
expects `RuntimeError` containing `prompt protocol`. Add another that changes
|
||||
`max_tokens` and expects `RuntimeError` containing `sampling settings`.
|
||||
|
||||
- [ ] **Step 2: Run the focused tests and verify RED**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python -m unittest discover \
|
||||
-s tests/model_executor/offloader \
|
||||
-p test_accuracy_repro.py
|
||||
```
|
||||
|
||||
Expected: failures because summary and cross-case protocol checks do not exist.
|
||||
|
||||
- [ ] **Step 3: Persist diagnostics and enforce matrix consistency**
|
||||
|
||||
Write `parsed_answer`, `output_token_count`, and `finish_reason` to each example
|
||||
JSONL row. Add protocol and summary objects to each result JSON. In
|
||||
`verify_matrix`, capture baseline protocol and sampling settings, then reject a
|
||||
candidate when either differs.
|
||||
|
||||
Change defaults:
|
||||
|
||||
```bash
|
||||
GSM8K_MAX_TOKENS=${GSM8K_MAX_TOKENS:-512}
|
||||
GPQA_MAX_TOKENS=${GPQA_MAX_TOKENS:-8}
|
||||
```
|
||||
|
||||
and change the Python CLI `--max-tokens` default to `512`.
|
||||
|
||||
- [ ] **Step 4: Run the focused tests and verify GREEN**
|
||||
|
||||
Run the command from Step 2. Expected: pass.
|
||||
|
||||
- [ ] **Step 5: Run all relevant local verification**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python -m unittest discover \
|
||||
-s tests/model_executor/offloader \
|
||||
-p test_accuracy_repro.py
|
||||
python -m compileall -q scripts/accuracy_gsm8k_gpqa.py scripts/verify_accuracy_matrix.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Expected: all unit tests pass, compilation exits 0, and `git diff --check`
|
||||
produces no output.
|
||||
|
||||
- [ ] **Step 6: Commit Task 3**
|
||||
|
||||
```bash
|
||||
git add \
|
||||
scripts/accuracy_gsm8k_gpqa.py \
|
||||
scripts/run_accuracy_gsm8k_gpqa.sh \
|
||||
scripts/verify_accuracy_matrix.py \
|
||||
tests/model_executor/offloader/test_accuracy_repro.py
|
||||
git commit -m "test: record and enforce formal accuracy protocol"
|
||||
```
|
||||
|
||||
### Task 4: Ascend Reproduction And Evidence Refresh
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/v018-deployment-and-reproduction.md`
|
||||
- Modify: `docs/v018-test-evidence.md`
|
||||
- Modify: `docs/finals/决赛项目结项书.md`
|
||||
- Produce outside Git: formal server logs and JSON results under a new dated
|
||||
reproduction directory.
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: the `qwen-chat-v1` scripts and the existing Qwen2.5-1.5B-Instruct
|
||||
model on the healthy Ascend server.
|
||||
- Produces: one full three-case GSM8K/GPQA matrix and updated evidence documents.
|
||||
|
||||
- [ ] **Step 1: Deploy the committed branch to the server**
|
||||
|
||||
Fetch the branch on the server and verify that its HEAD equals the local
|
||||
implementation commit. Do not place credentials in shell history or repository
|
||||
files.
|
||||
|
||||
- [ ] **Step 2: Run server-side unit tests**
|
||||
|
||||
Activate the v0.18 environment and run:
|
||||
|
||||
```bash
|
||||
python -m unittest discover \
|
||||
-s tests/model_executor/offloader \
|
||||
-p test_accuracy_repro.py
|
||||
```
|
||||
|
||||
Expected: all tests pass.
|
||||
|
||||
- [ ] **Step 3: Run a two-example baseline smoke test**
|
||||
|
||||
Invoke the formal runner with `ACCURACY_LIMIT=2` into a dedicated smoke result
|
||||
directory. Inspect both result JSON files and confirm:
|
||||
|
||||
- `prompt_protocol.name` is `qwen-chat-v1`.
|
||||
- GSM8K uses 512 tokens; GPQA uses 8.
|
||||
- Example JSONL rows contain parsed answer and generation diagnostics.
|
||||
|
||||
- [ ] **Step 4: Run the full formal matrix**
|
||||
|
||||
Use the existing full-dataset entry point with `ACCURACY_LIMIT=0` and a new
|
||||
dated result directory. Keep `MAX_ACCURACY_DROP=0`.
|
||||
|
||||
Expected:
|
||||
|
||||
- 1319 GSM8K rows and 198 GPQA Diamond rows per case.
|
||||
- Matrix verification reports `all_passed: true`.
|
||||
- Candidate accuracies equal their baseline for both datasets.
|
||||
- GSM8K no longer has near-total length truncation.
|
||||
- GPQA parsed count is nonzero and reflects explicit leading choices.
|
||||
|
||||
- [ ] **Step 5: Sync and independently inspect artifacts**
|
||||
|
||||
Download the new result directory locally. Recompute hashes for all six example
|
||||
JSONL files, inspect result metadata, and calculate exact correct/total and
|
||||
length-limit counts from the downloaded artifacts.
|
||||
|
||||
- [ ] **Step 6: Refresh evidence documents with measured values**
|
||||
|
||||
Replace obsolete `51/1319` and `0/198` claims in the three listed Markdown
|
||||
documents with values read from the new formal JSON results. State the exact
|
||||
protocol, token budgets, parsed counts, truncation counts, and the relative
|
||||
offload accuracy change. Do not modify the PPTX or DOCX in this task.
|
||||
|
||||
- [ ] **Step 7: Verify docs and repository state**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
rg -n "qwen-chat-v1|GSM8K_MAX_TOKENS|GPQA_MAX_TOKENS|accuracy" \
|
||||
docs/v018-deployment-and-reproduction.md \
|
||||
docs/v018-test-evidence.md \
|
||||
docs/finals/决赛项目结项书.md
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Expected: all three documents cite the corrected protocol and measured results;
|
||||
the only uncommitted changes are the intended evidence updates.
|
||||
|
||||
- [ ] **Step 8: Commit the refreshed evidence**
|
||||
|
||||
```bash
|
||||
git add \
|
||||
docs/v018-deployment-and-reproduction.md \
|
||||
docs/v018-test-evidence.md \
|
||||
docs/finals/决赛项目结项书.md
|
||||
git commit -m "docs: refresh corrected accuracy evidence"
|
||||
```
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
# vLLM Ascend v0.23.0rc1 Deployment And Comparison Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use
|
||||
> `superpowers:subagent-driven-development` or `superpowers:executing-plans`
|
||||
> to execute this plan task by task.
|
||||
|
||||
**Goal:** Deploy the latest official vLLM Ascend release on the existing
|
||||
Ascend 910B server and produce a reproducible, same-workload comparison against
|
||||
the completed v0.18.0 measurements.
|
||||
|
||||
**Architecture:** Preserve the existing v0.18.0 runtime and evidence. Install
|
||||
the latest matched software stack into separate versioned directories, deploy
|
||||
the upstream `v0.23.0rc1` source at its exact release commit, and run the same
|
||||
model, datasets, prompt protocol, seeds, offload cases, and performance
|
||||
workload. Store immutable environment manifests, raw logs, JSON results, and a
|
||||
cross-version comparison report in a new dated artifact directory.
|
||||
|
||||
**Tech Stack:** Ascend 910B2C, CANN 9.0.1, Python 3.12, PyTorch 2.10.0,
|
||||
torch-npu 2.10.0.post2, vLLM 0.23.0, vLLM Ascend 0.23.0rc1,
|
||||
triton-ascend 3.2.1, Bash, Python, JSON/JSONL.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Treat official tag `v0.23.0rc1` at commit
|
||||
`f4a08bddd0cc65a0bd8c3d377b158ae5ca7527db` as the tested latest release.
|
||||
- Use only the release's supported dependency matrix. Do not call a run
|
||||
"formal" if it uses the server's old CANN 9.0.0 userspace.
|
||||
- Keep `/data/vllm-ascend-v0.18.0` and all existing v0.18.0 results unchanged.
|
||||
- Install latest components under new versioned paths and record package
|
||||
versions, source commits, CANN version, driver version, OS, and NPU identity.
|
||||
- Reuse `/data/models/Qwen2.5-1.5B-Instruct` and the materialized datasets in
|
||||
`/data/accuracy-gsm8k-gpqa`; verify hashes before testing.
|
||||
- Compare exactly three cases: `baseline_0_1` `(0,1)`,
|
||||
`group_8_num_1` `(8,1)`, and `group_8_num_2` `(8,2)`.
|
||||
- Select the latest implementation explicitly with
|
||||
`offload_backend="prefetch"` for non-baseline cases.
|
||||
- Performance protocol: 3 repeats per case, round-robin case order, 1 warmup
|
||||
round, 3 measured rounds, 8 prompts, 128 input tokens, up to 64 output
|
||||
tokens, max model length 512, eager mode, seed 0, and memory utilization 0.5.
|
||||
- Accuracy protocol: full GSM8K 1319 and GPQA Diamond 198 examples,
|
||||
`qwen-chat-v1`, temperature 0, seed 0, 512/8 output-token budgets, and zero
|
||||
allowed accuracy drop between offload cases and the latest baseline.
|
||||
- Never store passwords, access tokens, or credential-bearing URLs in files,
|
||||
shell history, logs, commits, or reports.
|
||||
|
||||
## Artifact Layout
|
||||
|
||||
- Latest source: `/data/vllm-ascend-v0.23.0rc1`
|
||||
- Latest environment: `/data/vllm-ascend-v0.23.0rc1/.venv`
|
||||
- Latest results:
|
||||
`/data/reproduction-results/v0230rc1-comparison-20260730`
|
||||
- Local synchronized results:
|
||||
`D:\PDSL\Ascend\reproduction-results-v0230rc1-20260730`
|
||||
- Final report:
|
||||
`docs/v0230rc1-comparison.md`
|
||||
|
||||
### Task 1: Freeze Release And Audit The Host
|
||||
|
||||
- [ ] Record the official release/dependency matrix and local upstream commit.
|
||||
- [ ] Verify the feature branch is clean and record its exact commit.
|
||||
- [ ] Connect to the server and capture OS, CPU architecture, Python versions,
|
||||
disk space, NPU/driver health, installed CANN versions, and container
|
||||
availability.
|
||||
- [ ] Verify model path, model files, dataset counts, and dataset SHA-256 values.
|
||||
- [ ] Choose the supported isolated deployment route from observed host facts.
|
||||
|
||||
### Task 2: Install The Matched Latest Runtime
|
||||
|
||||
- [ ] Install CANN 9.0.1 userspace side by side if it is not already present.
|
||||
- [ ] Create an isolated Python 3.12 environment under the latest runtime path.
|
||||
- [ ] Install the exact matched PyTorch, torch-npu, vLLM, triton-ascend, and
|
||||
vLLM Ascend release source versions.
|
||||
- [ ] Save `pip freeze`, release commit, CANN version, environment variables,
|
||||
driver/NPU output, and checksums in the latest result directory.
|
||||
- [ ] Import `torch`, `torch_npu`, `vllm`, and `vllm_ascend`; allocate a small
|
||||
tensor on the selected NPU; verify the current v0.18.0 runtime still
|
||||
imports independently.
|
||||
|
||||
### Task 3: Adapt And Smoke-Test The Latest Parameter API
|
||||
|
||||
- [ ] Add a version-neutral way for benchmark and accuracy runners to pass
|
||||
`offload_backend="prefetch"` only when requested.
|
||||
- [ ] Add focused local unit tests for the argument construction and defaults.
|
||||
- [ ] Deploy the committed/current scripts to an isolated server checkout.
|
||||
- [ ] Run the upstream offloader-focused unit or E2E test supported by the
|
||||
available local model, followed by one baseline and two offload smoke
|
||||
generations.
|
||||
- [ ] Confirm logs show the official `NPUPrefetchOffloader`, expected group
|
||||
values, non-empty output, and no remaining NPU process.
|
||||
|
||||
### Task 4: Run The Matched Performance Matrix
|
||||
|
||||
- [ ] Run the fixed three-case performance protocol under v0.23.0rc1.
|
||||
- [ ] Preserve one raw JSONL row and full log per process/repeat.
|
||||
- [ ] Validate nine successful runs, identical workload metadata, finite
|
||||
throughput/latency, and recorded memory samples.
|
||||
- [ ] Produce median and spread summaries plus deltas versus latest baseline
|
||||
and the completed v0.18.0 matrix.
|
||||
|
||||
### Task 5: Run The Full Accuracy Matrix
|
||||
|
||||
- [ ] Verify dataset hashes immediately before the run.
|
||||
- [ ] Run all six full dataset/case combinations under v0.23.0rc1.
|
||||
- [ ] Run the strict matrix verifier with zero allowed drop.
|
||||
- [ ] Validate result counts, prompt/sampling contracts, parsed-answer counts,
|
||||
truncation diagnostics, and example JSONL row counts.
|
||||
- [ ] Preserve raw examples, summaries, verifier output, and full run logs.
|
||||
|
||||
### Task 6: Synchronize And Report Evidence
|
||||
|
||||
- [ ] Download the complete latest result directory to the local artifact path.
|
||||
- [ ] Recompute hashes and independently parse all JSON/JSONL artifacts locally.
|
||||
- [ ] Write `docs/v0230rc1-comparison.md` with exact environment, commands,
|
||||
results, cross-version deltas, limitations, and artifact checksums.
|
||||
- [ ] Run local unit tests, Python compilation, JSON validation,
|
||||
`git diff --check`, and repository status inspection.
|
||||
- [ ] Review the report against every global constraint before declaring the
|
||||
deployment and test complete.
|
||||
|
|
@ -0,0 +1,582 @@
|
|||
# Qwen2.5-7B Dual-Version Parameter Performance Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Reproducibly benchmark the current offloader and official vLLM Ascend v0.23.0rc1 across the same Qwen2.5-7B parameter matrix, then separate same-runtime implementation effects from native-stack version effects.
|
||||
|
||||
**Architecture:** A shell entry point creates an isolated current-implementation overlay on the official v0.23 runtime, executes three interleaved benchmark phases, and records one JSON object and one log per process. A dependency-light Python analyzer validates the full run contract, computes capacity and throughput statistics, compares current and latest cases, and writes machine-readable and review-ready summaries.
|
||||
|
||||
**Tech Stack:** Bash, Python 3.11/3.12 standard library, vLLM, vLLM Ascend, PyTorch/torch-npu, safetensors, CANN 9.0.1, Ascend 910B2C, unittest/pytest.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Model: Qwen2.5-7B-Instruct, 28 layers, FP16.
|
||||
- Hardware: one Ascend 910B2C, physical device 6 / logical device 0.
|
||||
- Current native target: vLLM Ascend v0.18-compatible project implementation.
|
||||
- Latest target: official vLLM Ascend v0.23.0rc1 at commit `f4a08bd`, with vLLM 0.23.0.
|
||||
- Same-runtime cases must differ only in the offloader package overlay and required class alias.
|
||||
- Workload: 8 prompts, 128 input tokens, 64 maximum output tokens, `max_model_len=512`, `gpu_memory_utilization=0.5`, eager mode, one warm-up round, seed 0.
|
||||
- Main matrix: 63 processes; long-window phase: 15 processes; native-v0.18 phase: 12 processes.
|
||||
- Do not modify or revert pre-existing dirty files.
|
||||
- Do not describe deltas smaller than observed variation as proven speedups.
|
||||
- Preserve every raw log and generate SHA-256 manifests before transfer.
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
- Create `scripts/analyze_large_model_parameter_comparison.py`
|
||||
- Owns matrix contracts, layer selection, model-weight accounting, log parsing,
|
||||
aggregation, implementation comparisons, Pareto classification, validation,
|
||||
and CSV/JSON/Markdown output.
|
||||
- Create `scripts/run_large_model_parameter_comparison.sh`
|
||||
- Owns environment setup, overlay construction, phase scheduling, process
|
||||
isolation, environment capture, analyzer invocation, and result manifest.
|
||||
- Create `tests/model_executor/offloader/test_large_model_parameter_comparison.py`
|
||||
- Unit-tests analyzer calculations and statically validates the shell matrix.
|
||||
- Create `docs/finals/qwen25-7b-current-vs-latest-parameter-performance.md`
|
||||
- Records the final measured results, interpretation, limitations, and
|
||||
reproduction commands.
|
||||
- Do not modify `scripts/bench_one.py`
|
||||
- Reuse its existing deterministic generation and resource measurements.
|
||||
|
||||
### Task 1: Analyzer Contracts and Layer Accounting
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/analyze_large_model_parameter_comparison.py`
|
||||
- Create: `tests/model_executor/offloader/test_large_model_parameter_comparison.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `select_layers(num_layers: int, group_size: int, num_in_group: int) -> tuple[int, ...]`
|
||||
- Produces: `read_layer_weight_bytes(model_path: Path, num_layers: int) -> dict[int, int]`
|
||||
- Produces: `capacity_for_layers(layer_bytes: dict[int, int], layers: tuple[int, ...]) -> int`
|
||||
- Consumes: local Hugging Face-format model directory with safetensors files.
|
||||
|
||||
- [ ] **Step 1: Write failing selection and capacity tests**
|
||||
|
||||
```python
|
||||
def test_select_layers_matches_the_two_parameter_semantics():
|
||||
analyzer = load_script("analyze_large_model_parameter_comparison.py")
|
||||
assert analyzer.select_layers(28, 14, 1) == (13, 27)
|
||||
assert analyzer.select_layers(28, 28, 2) == (26, 27)
|
||||
assert analyzer.select_layers(28, 0, 1) == ()
|
||||
|
||||
|
||||
def test_capacity_sums_only_selected_layer_weights():
|
||||
analyzer = load_script("analyze_large_model_parameter_comparison.py")
|
||||
layer_bytes = {index: (index + 1) * 100 for index in range(28)}
|
||||
assert analyzer.capacity_for_layers(layer_bytes, (13, 27)) == 4200
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the tests and verify the missing-module failure**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
python -m unittest tests/model_executor/offloader/test_large_model_parameter_comparison.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL because `analyze_large_model_parameter_comparison.py` does not
|
||||
exist.
|
||||
|
||||
- [ ] **Step 3: Implement selection and safetensors weight accounting**
|
||||
|
||||
Implement these constants and functions:
|
||||
|
||||
```python
|
||||
PARAMETERS = (
|
||||
(0, 1), (28, 1), (14, 1), (28, 2), (8, 1), (7, 1),
|
||||
(14, 2), (8, 2), (4, 1), (2, 1), (4, 2),
|
||||
)
|
||||
|
||||
|
||||
def select_layers(num_layers, group_size, num_in_group):
|
||||
if group_size == 0 or num_in_group == 0:
|
||||
return ()
|
||||
if group_size < 0 or num_in_group < 0 or num_in_group > group_size:
|
||||
raise ValueError("invalid offload group parameters")
|
||||
cutoff = group_size - num_in_group
|
||||
return tuple(
|
||||
index for index in range(num_layers)
|
||||
if index % group_size >= cutoff
|
||||
)
|
||||
|
||||
|
||||
def capacity_for_layers(layer_bytes, layers):
|
||||
return sum(layer_bytes[index] for index in layers)
|
||||
```
|
||||
|
||||
`read_layer_weight_bytes` must iterate every `*.safetensors` file with
|
||||
`safetensors.safe_open`, match tensor names against
|
||||
`r"^model\.layers\.(\d+)\."`, and compute bytes from tensor shape and dtype.
|
||||
Reject a model when any layer from 0 through 27 has zero accounted bytes.
|
||||
|
||||
- [ ] **Step 4: Run focused tests**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
python -m unittest tests/model_executor/offloader/test_large_model_parameter_comparison.py -v
|
||||
```
|
||||
|
||||
Expected: selection and capacity tests PASS.
|
||||
|
||||
- [ ] **Step 5: Commit Task 1**
|
||||
|
||||
```bash
|
||||
git add scripts/analyze_large_model_parameter_comparison.py \
|
||||
tests/model_executor/offloader/test_large_model_parameter_comparison.py
|
||||
git commit -m "test: add large-model comparison contracts"
|
||||
```
|
||||
|
||||
### Task 2: Aggregation, Comparison, and Validation
|
||||
|
||||
**Files:**
|
||||
- Modify: `scripts/analyze_large_model_parameter_comparison.py`
|
||||
- Modify: `tests/model_executor/offloader/test_large_model_parameter_comparison.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: `load_jsonl(path: Path) -> list[dict[str, object]]`
|
||||
- Produces: `aggregate_label(records: list[dict[str, object]]) -> dict[str, object]`
|
||||
- Produces: `compare_implementations(rows: list[dict[str, object]]) -> list[dict[str, object]]`
|
||||
- Produces: `validate_results(results_dir: Path) -> dict[str, object]`
|
||||
- Produces files: `comparison-summary.json`, `comparison-summary.csv`,
|
||||
`comparison-summary.md`, and `validation.json`.
|
||||
|
||||
- [ ] **Step 1: Add failing aggregation and comparison tests**
|
||||
|
||||
Use synthetic records with process throughputs `100`, `110`, and `90`, and
|
||||
round latencies that yield pooled throughputs `80`, `100`, and `120`.
|
||||
|
||||
```python
|
||||
def test_aggregate_label_uses_process_and_round_medians():
|
||||
analyzer = load_script("analyze_large_model_parameter_comparison.py")
|
||||
row = analyzer.aggregate_label(make_records([100.0, 110.0, 90.0]))
|
||||
assert row["throughput_median_tok_s"] == 100.0
|
||||
assert row["repeat_count"] == 3
|
||||
assert row["pooled_round_count"] == 9
|
||||
|
||||
|
||||
def test_compare_implementations_pairs_equal_parameters():
|
||||
analyzer = load_script("analyze_large_model_parameter_comparison.py")
|
||||
rows = [
|
||||
{"implementation": "current", "group_size": 14,
|
||||
"num_in_group": 1, "throughput_median_tok_s": 105.0},
|
||||
{"implementation": "latest", "group_size": 14,
|
||||
"num_in_group": 1, "throughput_median_tok_s": 100.0},
|
||||
]
|
||||
comparison = analyzer.compare_implementations(rows)
|
||||
assert comparison[0]["current_vs_latest_percent"] == 5.0
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run focused tests and verify failure**
|
||||
|
||||
Run the unittest command from Task 1.
|
||||
|
||||
Expected: FAIL because aggregation and comparison functions are missing.
|
||||
|
||||
- [ ] **Step 3: Implement parsing and statistics**
|
||||
|
||||
For each label calculate:
|
||||
|
||||
- Process throughput list, median, mean, sample standard deviation, min, max.
|
||||
- Pooled measured-round throughput median, mean, sample standard deviation.
|
||||
- Median P50/P95 latency, load time, and available KV-cache GiB.
|
||||
- Selected layers, selected-layer count, and exact selected weight bytes.
|
||||
- Throughput change against the phase baseline.
|
||||
|
||||
Pair same-runtime `current` and `latest` rows by `(group_size, num_in_group)`.
|
||||
Calculate both absolute tok/s and percentage deltas. Mark a point Pareto-optimal
|
||||
when no point in the same implementation has at least as much released weight
|
||||
and at least as much throughput, with one strict improvement.
|
||||
|
||||
- [ ] **Step 4: Add failing validation tests**
|
||||
|
||||
Create a temporary result tree with exact synthetic counts, then remove one log
|
||||
and assert:
|
||||
|
||||
```python
|
||||
validation = analyzer.validate_results(root)
|
||||
assert validation["valid"] is False
|
||||
assert validation["same_runtime_log_count"] == 62
|
||||
assert "same-runtime log count" in validation["errors"]
|
||||
```
|
||||
|
||||
Also test that one traceback, two `BENCH_RESULT` lines, or an incorrect
|
||||
implementation class makes validation fail.
|
||||
|
||||
- [ ] **Step 5: Implement exact result contracts**
|
||||
|
||||
Validation constants:
|
||||
|
||||
```python
|
||||
EXPECTED_SAME_RUNTIME = {"baseline": 3, **{
|
||||
f"{impl}_{group}_{num}": 3
|
||||
for impl in ("current", "latest")
|
||||
for group, num in PARAMETERS[1:]
|
||||
}}
|
||||
EXPECTED_LONG_WINDOW = {
|
||||
"baseline": 3,
|
||||
"current_28_1": 3, "latest_28_1": 3,
|
||||
"current_14_1": 3, "latest_14_1": 3,
|
||||
}
|
||||
EXPECTED_NATIVE_V018 = {
|
||||
"baseline": 3,
|
||||
"current_28_1": 3,
|
||||
"current_14_1": 3,
|
||||
"current_8_1": 3,
|
||||
}
|
||||
```
|
||||
|
||||
Require 63, 15, and 12 JSONL records and matching log counts. Require one
|
||||
`BENCH_RESULT` per log, zero traceback logs, `AscendPrefetchOffloader` in all
|
||||
same-runtime current offload logs, and `NPUPrefetchOffloader` in all
|
||||
same-runtime latest offload logs.
|
||||
|
||||
- [ ] **Step 6: Run analyzer tests**
|
||||
|
||||
Run the unittest command from Task 1.
|
||||
|
||||
Expected: all analyzer tests PASS.
|
||||
|
||||
- [ ] **Step 7: Commit Task 2**
|
||||
|
||||
```bash
|
||||
git add scripts/analyze_large_model_parameter_comparison.py \
|
||||
tests/model_executor/offloader/test_large_model_parameter_comparison.py
|
||||
git commit -m "feat: analyze dual-version parameter performance"
|
||||
```
|
||||
|
||||
### Task 3: Reproduction Runner
|
||||
|
||||
**Files:**
|
||||
- Create: `scripts/run_large_model_parameter_comparison.sh`
|
||||
- Modify: `tests/model_executor/offloader/test_large_model_parameter_comparison.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: `MODEL_PATH`, `RESULTS_DIR`, `NPU_INDEX`,
|
||||
`ASCEND_RT_VISIBLE_DEVICES`, `CURRENT_V018_SOURCE_ROOT`,
|
||||
`LATEST_VENV_ACTIVATE`, and optional cache paths.
|
||||
- Produces: three JSONL files, 90 process logs, environment/model manifests,
|
||||
analysis summaries, validation, and `SHA256SUMS`.
|
||||
- Calls: `scripts/bench_one.py` and
|
||||
`scripts/analyze_large_model_parameter_comparison.py`.
|
||||
|
||||
- [ ] **Step 1: Add failing shell-contract test**
|
||||
|
||||
The test must read the shell file and assert:
|
||||
|
||||
```python
|
||||
assert "set -euo pipefail" in script
|
||||
assert "SAME_RUNTIME_CASES=(" in script
|
||||
assert "LONG_WINDOW_CASES=(" in script
|
||||
assert "NATIVE_V018_CASES=(" in script
|
||||
assert script.count("run_case ") >= 3
|
||||
assert "current_impl_overlay" in script
|
||||
assert "NPUPrefetchOffloader = AscendPrefetchOffloader" in script
|
||||
assert "analyze_large_model_parameter_comparison.py" in script
|
||||
assert "find \"${results_name}\" -type f ! -name SHA256SUMS" in script
|
||||
```
|
||||
|
||||
Parse the three arrays in the test and assert 21, 5, and 4 labels.
|
||||
|
||||
- [ ] **Step 2: Run the test and verify missing-file failure**
|
||||
|
||||
Run the unittest command from Task 1.
|
||||
|
||||
Expected: FAIL because the runner does not exist.
|
||||
|
||||
- [ ] **Step 3: Implement common setup and result protection**
|
||||
|
||||
The shell script must:
|
||||
|
||||
- Source `/data/Ascend/cann/set_env.sh` and optional ATB setup.
|
||||
- Default latest venv to
|
||||
`/data/vllm-ascend-v0.23.0rc1/.venv-cann901/bin/activate`.
|
||||
- Default v0.18 venv to
|
||||
`/data/vllm-ascend-v0.18.0/.venv/bin/activate`.
|
||||
- Refuse to run when `RESULTS_DIR/same-runtime-runs.jsonl` already exists.
|
||||
- Run `check_npu_health.sh` before each phase.
|
||||
- Set unique XDG, vLLM, Hugging Face, and Triton cache directories.
|
||||
- Capture package versions, NPU state, source hashes, model file hashes, and
|
||||
free disk space.
|
||||
|
||||
- [ ] **Step 4: Implement the latest-runtime overlay phase**
|
||||
|
||||
Locate the official package with `importlib.metadata.distribution`, copy it to
|
||||
`${RESULTS_DIR}.current_impl_overlay`, replace only
|
||||
`model_executor/offloader/`, and append the compatibility alias only when
|
||||
absent.
|
||||
|
||||
Use explicit `PYTHONPATH` per process:
|
||||
|
||||
- Official package parent for latest cases.
|
||||
- Overlay root for current cases.
|
||||
|
||||
Each positive-group case passes `--offload-backend prefetch`.
|
||||
|
||||
- [ ] **Step 5: Implement deterministic schedules**
|
||||
|
||||
Use three arrays:
|
||||
|
||||
```bash
|
||||
SAME_RUNTIME_CASES=(
|
||||
"baseline:official:0:1"
|
||||
"current_28_1:current:28:1" "latest_28_1:official:28:1"
|
||||
"current_14_1:current:14:1" "latest_14_1:official:14:1"
|
||||
"current_28_2:current:28:2" "latest_28_2:official:28:2"
|
||||
"current_8_1:current:8:1" "latest_8_1:official:8:1"
|
||||
"current_7_1:current:7:1" "latest_7_1:official:7:1"
|
||||
"current_14_2:current:14:2" "latest_14_2:official:14:2"
|
||||
"current_8_2:current:8:2" "latest_8_2:official:8:2"
|
||||
"current_4_1:current:4:1" "latest_4_1:official:4:1"
|
||||
"current_2_1:current:2:1" "latest_2_1:official:2:1"
|
||||
"current_4_2:current:4:2" "latest_4_2:official:4:2"
|
||||
)
|
||||
```
|
||||
|
||||
The long-window array contains baseline plus current/latest `(28,1)` and
|
||||
`(14,1)`. The native array contains baseline plus current `(28,1)`, `(14,1)`,
|
||||
and `(8,1)`.
|
||||
|
||||
For each of three repeats, rotate the start index and run every case once.
|
||||
|
||||
- [ ] **Step 6: Implement clean v0.18 native subshell**
|
||||
|
||||
Run the native phase in a subshell that sources only the v0.18 venv and sets
|
||||
`PYTHONPATH=${CURRENT_V018_SOURCE_ROOT}`. Before starting, compare SHA-256 for
|
||||
the deployed offloader and runner against the project source recorded in the
|
||||
environment manifest. Stop on a mismatch instead of silently testing stale
|
||||
code.
|
||||
|
||||
- [ ] **Step 7: Analyze and create manifest**
|
||||
|
||||
Invoke the analyzer only after all phases complete. Copy the runner, analyzer,
|
||||
`bench_one.py`, and analyzer test into `${RESULTS_DIR}/reproduction/`, then
|
||||
create `SHA256SUMS` with sorted relative paths.
|
||||
|
||||
- [ ] **Step 8: Run local tests and remote syntax check**
|
||||
|
||||
Local:
|
||||
|
||||
```powershell
|
||||
python -m unittest tests/model_executor/offloader/test_large_model_parameter_comparison.py -v
|
||||
```
|
||||
|
||||
Remote:
|
||||
|
||||
```bash
|
||||
bash -n scripts/run_large_model_parameter_comparison.sh
|
||||
```
|
||||
|
||||
Expected: all tests PASS and syntax check exits 0.
|
||||
|
||||
- [ ] **Step 9: Commit Task 3**
|
||||
|
||||
```bash
|
||||
git add scripts/run_large_model_parameter_comparison.sh \
|
||||
tests/model_executor/offloader/test_large_model_parameter_comparison.py
|
||||
git commit -m "feat: add Qwen 7B comparison runner"
|
||||
```
|
||||
|
||||
### Task 4: Model Download and Smoke Validation
|
||||
|
||||
**Files:**
|
||||
- No repository edits.
|
||||
- Create remotely: `/data/models/Qwen2.5-7B-Instruct/`
|
||||
- Create remotely: `/data/reproduction-results/qwen25-7b-smoke/`
|
||||
|
||||
**Interfaces:**
|
||||
- Produces: a complete local model directory and smoke logs accepted by Task 3.
|
||||
|
||||
- [ ] **Step 1: Recheck disk and NPU**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
df -h /data
|
||||
npu-smi info
|
||||
```
|
||||
|
||||
Require at least 25 GiB free before download, NPU health `OK`, and no running
|
||||
NPU process.
|
||||
|
||||
- [ ] **Step 2: Download the pinned model snapshot**
|
||||
|
||||
Use ModelScope in a dedicated download environment:
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 -m venv /data/model-download-venv
|
||||
source /data/model-download-venv/bin/activate
|
||||
python -m pip install "modelscope>=1.28,<2"
|
||||
modelscope download \
|
||||
--model Qwen/Qwen2.5-7B-Instruct \
|
||||
--local_dir /data/models/Qwen2.5-7B-Instruct
|
||||
```
|
||||
|
||||
Record the resolved repository revision emitted by ModelScope.
|
||||
|
||||
- [ ] **Step 3: Verify model completeness**
|
||||
|
||||
Require `config.json`, tokenizer files, `model.safetensors.index.json`, and
|
||||
every shard named by the index. Generate:
|
||||
|
||||
```bash
|
||||
find /data/models/Qwen2.5-7B-Instruct \
|
||||
-type f ! -name SHA256SUMS -print0 \
|
||||
| sort -z | xargs -0 sha256sum \
|
||||
> /data/models/Qwen2.5-7B-Instruct/SHA256SUMS
|
||||
sha256sum -c /data/models/Qwen2.5-7B-Instruct/SHA256SUMS
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run four smoke cases**
|
||||
|
||||
Run one process each for:
|
||||
|
||||
1. v0.18 native baseline `(0,1)`.
|
||||
2. v0.23 official baseline `(0,1)`.
|
||||
3. v0.23 current overlay `(14,1)`.
|
||||
4. v0.23 official `(14,1)`.
|
||||
|
||||
Use one warm-up and one measured round. Require one `BENCH_RESULT`, no
|
||||
traceback, and the expected implementation class.
|
||||
|
||||
- [ ] **Step 5: Record smoke decision**
|
||||
|
||||
Write `/data/reproduction-results/qwen25-7b-smoke/validation.json` with the four
|
||||
return codes, class checks, model path, and `valid: true`. Do not start the
|
||||
formal matrix unless it is true.
|
||||
|
||||
### Task 5: Formal Remote Execution
|
||||
|
||||
**Files:**
|
||||
- Create remotely under:
|
||||
`/data/reproduction-results/qwen25-7b-current-vs-latest-20260731/`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes the model and scripts validated by Task 4.
|
||||
- Produces the complete raw dataset consumed by Task 6.
|
||||
|
||||
- [ ] **Step 1: Upload exact committed scripts**
|
||||
|
||||
Transfer the runner, analyzer, benchmark harness, and analyzer test. Compare
|
||||
their local and remote SHA-256 values before execution.
|
||||
|
||||
- [ ] **Step 2: Start the formal runner**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
MODEL_PATH=/data/models/Qwen2.5-7B-Instruct \
|
||||
RESULTS_DIR=/data/reproduction-results/qwen25-7b-current-vs-latest-20260731 \
|
||||
NPU_INDEX=6 \
|
||||
ASCEND_RT_VISIBLE_DEVICES=0 \
|
||||
CURRENT_V018_SOURCE_ROOT=/data/vllm-ascend-v0.18.0 \
|
||||
bash scripts/run_large_model_parameter_comparison.sh
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Monitor phase checkpoints**
|
||||
|
||||
After each phase, verify:
|
||||
|
||||
- Same-runtime: 63 JSONL lines and 63 logs.
|
||||
- Long-window: 15 JSONL lines and 15 logs.
|
||||
- Native-v0.18: 12 JSONL lines and 12 logs.
|
||||
- NPU health remains `OK`.
|
||||
|
||||
Do not delete partial results after a failure.
|
||||
|
||||
- [ ] **Step 4: Run offloader regression tests**
|
||||
|
||||
In the v0.18 environment:
|
||||
|
||||
```bash
|
||||
python -m pytest tests/model_executor/offloader -q
|
||||
```
|
||||
|
||||
Save output as `offloader-tests-v018.log`.
|
||||
|
||||
- [ ] **Step 5: Independently recalculate key medians**
|
||||
|
||||
Use a separate Python command over the three JSONL files to print per-label
|
||||
process medians and pooled-round medians. Compare them against
|
||||
`comparison-summary.json`; require differences below `0.001 tok/s`.
|
||||
|
||||
### Task 6: Report, Archive, and Local Verification
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/finals/qwen25-7b-current-vs-latest-parameter-performance.md`
|
||||
- Modify only if defects are found:
|
||||
`scripts/analyze_large_model_parameter_comparison.py`
|
||||
- Modify only if defects are found:
|
||||
`tests/model_executor/offloader/test_large_model_parameter_comparison.py`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: validated remote summaries and raw logs.
|
||||
- Produces: final report, compressed archive, and locally verified extraction.
|
||||
|
||||
- [ ] **Step 1: Write the result-led report**
|
||||
|
||||
The report must include:
|
||||
|
||||
- Exact model, hardware, runtime, and source identities.
|
||||
- Same-runtime current vs latest table for all ten positive-group points.
|
||||
- Native v0.18 vs native v0.23 table for four practical points.
|
||||
- Long-window low-density table.
|
||||
- Capacity-throughput Pareto frontiers and knee.
|
||||
- Same-layer spaced vs adjacent comparisons.
|
||||
- A direct statement on whether the current implementation proves a measurable
|
||||
improvement over latest.
|
||||
- Custom-op or other runtime warnings and their impact on interpretation.
|
||||
- Exact reproduction command and artifact paths.
|
||||
|
||||
- [ ] **Step 2: Run final validation**
|
||||
|
||||
Require `validation.json` to report `valid: true`. Run:
|
||||
|
||||
```bash
|
||||
bash -n scripts/run_large_model_parameter_comparison.sh
|
||||
python -m pytest tests/model_executor/offloader -q
|
||||
```
|
||||
|
||||
Run local unittest and `git diff --check`.
|
||||
|
||||
- [ ] **Step 3: Copy the final report into raw reproduction artifacts**
|
||||
|
||||
Place the report in `${RESULTS_DIR}/reproduction/`, regenerate `SHA256SUMS`,
|
||||
and require every entry to pass `sha256sum -c`.
|
||||
|
||||
- [ ] **Step 4: Archive and hash**
|
||||
|
||||
Create:
|
||||
|
||||
```bash
|
||||
cd /data/reproduction-results
|
||||
tar -czf /data/qwen25-7b-current-vs-latest-20260731-results.tar.gz \
|
||||
qwen25-7b-current-vs-latest-20260731
|
||||
sha256sum /data/qwen25-7b-current-vs-latest-20260731-results.tar.gz
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Download and verify locally**
|
||||
|
||||
Download the archive to `D:\PDSL\Ascend`, verify its top-level SHA-256, extract
|
||||
it, verify every manifest entry, and rerun the analyzer's core aggregation
|
||||
functions without modifying the extracted evidence.
|
||||
|
||||
- [ ] **Step 6: Commit repository artifacts**
|
||||
|
||||
Stage only the new runner, analyzer, tests, and final report:
|
||||
|
||||
```bash
|
||||
git add scripts/run_large_model_parameter_comparison.sh \
|
||||
scripts/analyze_large_model_parameter_comparison.py \
|
||||
tests/model_executor/offloader/test_large_model_parameter_comparison.py \
|
||||
docs/finals/qwen25-7b-current-vs-latest-parameter-performance.md
|
||||
git commit -m "perf: compare Qwen 7B offload parameters"
|
||||
```
|
||||
|
||||
- [ ] **Step 7: Final evidence summary**
|
||||
|
||||
Report the recommended parameter, exact current-vs-latest deltas, run counts,
|
||||
test counts, warning state, archive path, and archive SHA-256. Explicitly state
|
||||
whether changes were pushed; do not push unless the user requests it.
|
||||
|
|
@ -0,0 +1,392 @@
|
|||
# 决赛结项材料更新 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 将决赛结项书与 12 页答辩 PPT 更新为同时覆盖 Qwen2.5-1.5B 正式精度验证和 Qwen2.5-7B current/latest 性能对比的最终材料。
|
||||
|
||||
**Architecture:** 以结构化事实清单作为单一数据源,先更新 Markdown,再从 Markdown 重建 DOCX,并使用同一事实清单更新模板继承式 PPT。最后通过 OOXML 文本抽取、全页渲染、边界检查和人工视觉审查验证三份材料的内容与版式。
|
||||
|
||||
**Tech Stack:** Markdown、Git、Python 3、python-docx、OOXML、LibreOffice/Poppler、Node.js、`@oai/artifact-tool`、赛事 PPTX 模板。
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- 主标题保持为 `[功能] 适配 offload_group_size 和 offload_num_in_group 到 Ascend NPU`。
|
||||
- 副标题保持为 `vLLM Ascend 权重分组预取卸载参数适配与优化`。
|
||||
- 团队名称保持为 `乘风破浪的大学生`。
|
||||
- 目标版本是 vLLM Ascend v0.18.0;性能对比的 official latest 固定为 v0.23.0rc1 提交 `f4a08bd`。
|
||||
- 精度证据来自 Qwen2.5-1.5B-Instruct;7B 性能实验未采集精度,不得写成 7B 通过 GSM8K/GPQA。
|
||||
- 当前实现相对 official latest 没有可确认的稳定速度优势,不得将微小正差包装为提速结论。
|
||||
- PPT 使用 `D:\PDSL\Ascend\CCF大赛-vLLM Ascend赛道PPT模板.pptx`的主题、比例和品牌元素,保持 12 页。
|
||||
- 三份最终交付物位于 `docs/finals/`;内部构建与渲染文件位于 `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\`。
|
||||
- 不修改核心实现、测试脚本和原始实验证据;不把大型结果归档加入 Git。
|
||||
- 完成后可提交到本地当前分支;未经用户明确要求不执行远程推送。
|
||||
|
||||
---
|
||||
|
||||
## File Map
|
||||
|
||||
- Modify: `docs/finals/决赛项目结项书.md` - 三份材料的内容权威源。
|
||||
- Modify: `docs/finals/决赛项目结项书.docx` - 从 Markdown 重建的正式报告。
|
||||
- Modify: `docs/finals/决赛答辩PPT.pptx` - 沿用赛事模板的 12 页答辩文件。
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\evidence.json` - 可机器校验的版本、精度、性能和归档事实。
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\evidence-summary.md` - 供人工审阅的事实清单。
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_report_refresh.py` - 更新版 Markdown→DOCX 构建器。
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_deck_refresh.mjs` - 更新版模板继承式 PPT 构建器。
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\verify_final_materials.py` - 抽取 Markdown/DOCX/PPTX 文本并检查关键口径。
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\docx-render\` - DOCX 全页 PNG 和 PDF。
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\ppt-render\` - PPT 全页 PNG。
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\visual-review.md` - 逐页视觉审查记录。
|
||||
|
||||
### Task 1: 建立双证据事实清单
|
||||
|
||||
**Files:**
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\evidence.json`
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\evidence-summary.md`
|
||||
- Read: `docs/finals/qwen25-7b-current-vs-latest-parameter-performance.md`
|
||||
- Read: `docs/finals/parameter-optimization-performance.md`
|
||||
- Read: `docs/v018-test-evidence.md`
|
||||
- Read: `docs/finals/决赛项目结项书.md`
|
||||
- Read: `D:\PDSL\Ascend\qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8-results.tar.gz`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: 仓库内已提交报告、Git 历史和 hardened-v6 最终归档。
|
||||
- Produces: `evidence.json` 中的 `versions`、`accuracy`、`performance`、`validation`、`archive` 对象,供后续文档和校验器使用。
|
||||
|
||||
- [ ] **Step 1: 固定 Git 身份和开发时间线**
|
||||
|
||||
Run: `git rev-parse HEAD`
|
||||
|
||||
Run: `git log --reverse --oneline f0ab805..HEAD`
|
||||
|
||||
Expected: HEAD 包含设计提交 `49065ee`,时间线覆盖精度协议修复、7B runner/analyzer、hardened-v6 证据加固与最终复现套件。
|
||||
|
||||
- [ ] **Step 2: 复核最终 7B 归档身份**
|
||||
|
||||
Run: `Get-FileHash 'D:\PDSL\Ascend\qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8-results.tar.gz' -Algorithm SHA256`
|
||||
|
||||
Expected: `A8CC0AE4AA506D5C75F97D891001006889554E6B4B14603758E0A5E2014521F3`。
|
||||
|
||||
- [ ] **Step 3: 复核最终 validation 与独立重算证据**
|
||||
|
||||
Run: `Get-Content -Raw 'D:\PDSL\Ascend\qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8-final-verified-extracted\qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8\validation.json'`
|
||||
|
||||
Run: `Select-String -Path 'D:\PDSL\Ascend\qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8-final-verified-extracted\qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8\independent-verification.log' -Pattern 'MAX_PROCESS_DIFF|MAX_POOLED_DIFF|MAX_COMPARISON_DIFF'`
|
||||
|
||||
Expected: `valid` 为 true,`model_files_verified` 为 true,记录/日志计数为 63 + 15 + 12;三个 `MAX_*_DIFF` 均为 `0.000000000000`。
|
||||
|
||||
- [ ] **Step 4: 写入 `evidence.json` 和 `evidence-summary.md`**
|
||||
|
||||
记录以下精确值:
|
||||
|
||||
```json
|
||||
{
|
||||
"accuracy_model": "Qwen2.5-1.5B-Instruct",
|
||||
"gsm8k": {"correct": 953, "total": 1319, "accuracy_pct": 72.2517},
|
||||
"gpqa_diamond": {"correct": 47, "total": 198, "accuracy_pct": 23.7374},
|
||||
"performance_model": "Qwen/Qwen2.5-7B-Instruct",
|
||||
"processes": {"same_runtime": 63, "long_window": 15, "native_v018": 12, "total": 90},
|
||||
"same_runtime_diff_pct_range": [-0.086595, 0.043851],
|
||||
"knee": {"group": 28, "num": 1, "offloaded_gib": 0.434, "kv_cache_gib": 16.09, "throughput_retained_pct": 85.770046},
|
||||
"archive_sha256": "a8cc0ae4aa506d5c75f97d891001006889554e6b4b14603758e0a5e2014521f3"
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 扫描事实清单的占位符和数字矛盾**
|
||||
|
||||
Run: `rg -n "51/1319|0/198|3\.8666|742\.77|678\.85|383\.49" 'D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh'`
|
||||
|
||||
Expected: 无占位符;旧精度和旧 PPT 主性能数字不出现在新事实清单中。
|
||||
|
||||
### Task 2: 更新结项书 Markdown
|
||||
|
||||
**Files:**
|
||||
- Modify: `docs/finals/决赛项目结项书.md`
|
||||
- Read: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\evidence.json`
|
||||
- Read: `docs/finals/qwen25-7b-current-vs-latest-parameter-performance.md`
|
||||
- Read: `docs/finals/parameter-optimization-performance.md`
|
||||
- Read: `docs/superpowers/specs/2026-08-03-finals-materials-refresh-design.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 的固定事实。
|
||||
- Produces: 七章结构的最新 Markdown,供 DOCX 和 PPT 构建器使用。
|
||||
|
||||
- [ ] **Step 1: 更新封面元数据与摘要**
|
||||
|
||||
将代码与复现套件提交写为 `851feb5`,将 7B 实测部署提交单独写为 `ec673f8`,避免将后续文档提交写成代码身份。日期更新为 `2026 年 8 月`。摘要先给出 1.5B 零精度下降,再给出 7B `(28,1)` 容量膝点和“无稳定速度优势”结论。
|
||||
|
||||
- [ ] **Step 2: 更新项目概述与开发历史**
|
||||
|
||||
在第一章加入四阶段历程:v0.18.0 参数适配、`qwen-chat-v1` 精度协议修复、Qwen2.5-7B 同运行时 current/latest 对比、hardened-v6 身份绑定与独立重算。
|
||||
|
||||
- [ ] **Step 3: 补充技术方案中的证据链**
|
||||
|
||||
说明 runtime evidence hook 直接从实际 offloader 对象捕获 `selected_layers`、`wrapped_count`、`offloaded_bytes` 和实际类名;说明 CANN、wheel/package tree、latest/native source tree、模型清单和 runner 的 fail-closed 身份绑定。
|
||||
|
||||
- [ ] **Step 4: 更新实际交付清单**
|
||||
|
||||
补充 `scripts/run_large_model_parameter_comparison.sh`、`scripts/analyze_large_model_parameter_comparison.py`、`scripts/run_current_vs_latest_performance.sh`、`scripts/run_parameter_optimization_benchmark.sh` 及对应单测。
|
||||
|
||||
- [ ] **Step 5: 重写测试结论为双证据结构**
|
||||
|
||||
第五章固定为:测试环境与模型边界、功能/证据测试、1.5B GSM8K/GPQA、7B 参数容量/吞吐、current/latest 公平对比、native v0.18 辅助观察、归档与限制。
|
||||
|
||||
- [ ] **Step 6: 更新总结、限制与复现附录**
|
||||
|
||||
最终结论不宣称提速;将 `(28,1)` 定义为当前负载的实际容量膝点。附录增加 7B 对比精确命令、产物目录、`validation.json` 检查和最终归档 SHA-256。
|
||||
|
||||
- [ ] **Step 7: 执行 Markdown 事实与格式检查**
|
||||
|
||||
Run: `rg -n "72\.2517|23\.7374|90 条|0\.434 GiB|85\.77|v0\.23\.0rc1|-0\.086595|\+0\.043851|a8cc0ae4" docs/finals/决赛项目结项书.md`
|
||||
|
||||
Run: `rg -n "51/1319|0/198|3\.8666|优于 official latest|7B.*GSM8K|7B.*GPQA" docs/finals/决赛项目结项书.md`
|
||||
|
||||
Expected: 第一条命中所有关键事实;第二条不出现旧分数或越界表述。
|
||||
|
||||
Run: `git diff --check -- docs/finals/决赛项目结项书.md`
|
||||
|
||||
Expected: 无空白错误或冲突标记。
|
||||
|
||||
- [ ] **Step 8: 提交 Markdown 内容更新**
|
||||
|
||||
Run: `git add docs/finals/决赛项目结项书.md`
|
||||
|
||||
Run: `git commit -m "docs: refresh finals report evidence"`
|
||||
|
||||
### Task 3: 重建并验证 DOCX 结项书
|
||||
|
||||
**Files:**
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_report_refresh.py`
|
||||
- Modify: `docs/finals/决赛项目结项书.docx`
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\docx-render\`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 2 的 Markdown。
|
||||
- Produces: 内容一致、可打印且经过全页渲染的 DOCX。
|
||||
|
||||
- [ ] **Step 1: 阅读 `documents` skill 并加载工作区依赖**
|
||||
|
||||
使用本会话的 `documents:documents` skill,并调用 `codex_app__load_workspace_dependencies`。固定 Python 为 `C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe`。
|
||||
|
||||
- [ ] **Step 2: 从旧构建器创建 refresh 副本**
|
||||
|
||||
Source: `D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_report.py`
|
||||
|
||||
Destination: `D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_report_refresh.py`
|
||||
|
||||
保留封面、目录、页眉页脚、表格宽度、代码块和 Markdown 解析逻辑;将封面日期更新为 `2026 年 8 月`。
|
||||
|
||||
- [ ] **Step 3: 构建 DOCX**
|
||||
|
||||
Run: `& 'C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' 'D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_report_refresh.py'`
|
||||
|
||||
Expected: `docs/finals/决赛项目结项书.docx` 可正常打开,且文件大小大于 50 KB。
|
||||
|
||||
- [ ] **Step 4: 执行 DOCX 可访问性审计**
|
||||
|
||||
Run: `& 'C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' 'C:\Users\30780\.codex\plugins\cache\openai-primary-runtime\documents\26.802.11031\skills\documents\scripts\a11y_audit.py' 'docs/finals/决赛项目结项书.docx' --out_json 'D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\docx-a11y.json'`
|
||||
|
||||
Expected: 无阻断级错误;任何非阻断项写入视觉审查记录。
|
||||
|
||||
- [ ] **Step 5: 渲染 DOCX 全部页面**
|
||||
|
||||
Run: `& 'C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' 'C:\Users\30780\.codex\plugins\cache\openai-primary-runtime\documents\26.802.11031\skills\documents\render_docx.py' 'docs/finals/决赛项目结项书.docx' --output_dir 'D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\docx-render' --emit_pdf`
|
||||
|
||||
Expected: 每页均有 `page-*.png`,同目录生成 `决赛项目结项书.pdf`。
|
||||
|
||||
- [ ] **Step 6: 生成接触表并逐页审查**
|
||||
|
||||
Run: `& 'C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' 'D:\PDSL\Ascend\.codex_tmp\finals-materials\make_contact_sheets.py' 'D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\docx-render'`
|
||||
|
||||
使用 `view_image` 检查所有 `contact-*.png`,再对密度过高、表格跨页或代码换行页查看原始 `page-*.png`。检查封面、目录、孤立标题、表格截断、页眉页脚和异常空白页。
|
||||
|
||||
- [ ] **Step 7: 修复所有视觉问题并重新执行 Step 3-6**
|
||||
|
||||
Expected: 视觉审查记录中每个 DOCX 问题都标记为已修复或可接受且给出理由。
|
||||
|
||||
- [ ] **Step 8: 提交 DOCX**
|
||||
|
||||
Run: `git add docs/finals/决赛项目结项书.docx`
|
||||
|
||||
Run: `git commit -m "docs: rebuild finals report document"`
|
||||
|
||||
### Task 4: 重建 12 页决赛答辩 PPT
|
||||
|
||||
**Files:**
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_deck_refresh.mjs`
|
||||
- Modify: `docs/finals/决赛答辩PPT.pptx`
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\ppt-artifact-preview\`
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\ppt-artifact-layout\`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1 事实清单、Task 2 Markdown 和现有 `template-starter.pptx`。
|
||||
- Produces: 12 页模板继承式 PPTX,每页含建议时长、讲述要点和证据来源备注。
|
||||
|
||||
- [ ] **Step 1: 阅读 `presentations` skill 并固定 Node 运行时**
|
||||
|
||||
使用本会话的 `presentations:Presentations` skill。固定 Node 为 `C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe`,并复用 `D:\PDSL\Ascend\.codex_tmp\finals-materials\node_modules\@oai\artifact-tool`。
|
||||
|
||||
- [ ] **Step 2: 从旧构建器创建 refresh 副本**
|
||||
|
||||
Source: `D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_deck.mjs`
|
||||
|
||||
Destination: `D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_deck_refresh.mjs`
|
||||
|
||||
保留颜色、字体、模板导入、标题替换、讲者备注、预览和 layout 导出工具函数。
|
||||
|
||||
- [ ] **Step 3: 更新 12 页内容结构**
|
||||
|
||||
使用以下固定页序与时长,合计 680 秒:
|
||||
|
||||
| 页 | 主题 | 秒 |
|
||||
| ---: | --- | ---: |
|
||||
| 1 | 封面 | 20 |
|
||||
| 2 | 决赛成果总览 | 45 |
|
||||
| 3 | 参数语义与容量边界 | 45 |
|
||||
| 4 | Ascend 分组预取卸载架构 | 60 |
|
||||
| 5 | 开发历程与版本对比 | 55 |
|
||||
| 6 | 设计实现 | 75 |
|
||||
| 7 | 功能与证据验证 | 50 |
|
||||
| 8 | GSM8K / GPQA 精度 | 60 |
|
||||
| 9 | Qwen2.5-7B 容量与吞吐权衡 | 90 |
|
||||
| 10 | 当前实现 vs official latest | 75 |
|
||||
| 11 | 决赛阶段提升与工程价值 | 80 |
|
||||
| 12 | 总结与致谢 | 25 |
|
||||
|
||||
第 11 页必须将 AI 辅助定位为调用链阅读、可证伪假设、测试生成和证据审计工具,并明确所有结论由代码、日志和复现脚本约束。
|
||||
|
||||
- [ ] **Step 4: 更新精度与性能可视化**
|
||||
|
||||
第 8 页展示 1.5B 三组均为 GSM8K 953/1319(72.2517%)与 GPQA 47/198(23.7374%)。第 9 页展示 7B baseline、`(28,1)`、`(14,1)` 的 KV cache、吞吐与释放容量;将 `(28,1)` 标为当前负载膝点。第 10 页展示同运行时差范围 -0.086595% 至 +0.043851% 和长窗口 `(14,1)` +0.003119% / `(28,1)` -0.299468%。
|
||||
|
||||
- [ ] **Step 5: 写入讲者备注和证据来源**
|
||||
|
||||
每页备注包含 `建议时长`、`讲述要点`、`证据来源`。第 8 页来源为结项书和精度报告;第 9-10 页来源为 `docs/finals/qwen25-7b-current-vs-latest-parameter-performance.md` 和 hardened-v6 归档。
|
||||
|
||||
- [ ] **Step 6: 构建 PPTX 和 Artifact Tool 预览**
|
||||
|
||||
Run: `& 'C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\node\bin\node.exe' 'D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_deck_refresh.mjs'`
|
||||
|
||||
Expected: `docs/finals/决赛答辩PPT.pptx` 为 12 页,且 refresh 预览目录有 12 张 PNG 和 12 份 layout JSON。
|
||||
|
||||
### Task 5: 渲染和修复 PPT 版式
|
||||
|
||||
**Files:**
|
||||
- Modify: `D:\PDSL\Ascend\.codex_tmp\finals-materials\build_final_deck_refresh.mjs`
|
||||
- Modify: `docs/finals/决赛答辩PPT.pptx`
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\ppt-render\`
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\visual-review.md`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 4 PPTX。
|
||||
- Produces: 无溢出、无遮挡、可在 12 分钟内讲完的最终 PPTX。
|
||||
|
||||
- [ ] **Step 1: 使用 LibreOffice/Poppler 渲染全部幻灯片**
|
||||
|
||||
Run: `& 'C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' 'C:\Users\30780\.codex\plugins\cache\openai-primary-runtime\presentations\26.802.11031\skills\presentations\container_tools\render_slides.py' 'docs/finals/决赛答辩PPT.pptx' --output_dir 'D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\ppt-render'`
|
||||
|
||||
Expected: 生成 12 张 `slide-*.png`。
|
||||
|
||||
- [ ] **Step 2: 执行画布边界检查**
|
||||
|
||||
Run: `& 'C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' 'C:\Users\30780\.codex\plugins\cache\openai-primary-runtime\presentations\26.802.11031\skills\presentations\container_tools\slides_test.py' 'docs/finals/决赛答辩PPT.pptx'`
|
||||
|
||||
Expected: 不报告内容超出原始画布。
|
||||
|
||||
- [ ] **Step 3: 生成接触表并逐页审查**
|
||||
|
||||
Run: `& 'C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' 'D:\PDSL\Ascend\.codex_tmp\finals-materials\make_contact_sheets.py' 'D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\ppt-render'`
|
||||
|
||||
使用 `view_image` 检查所有接触表和数据密集页原图,重点检查第 5、8、9、10、11 页的文本边界、图表标签、颜色区分、单位和结论可读性。
|
||||
|
||||
- [ ] **Step 4: 修复溢出、重叠和密度问题**
|
||||
|
||||
仅修改 refresh 构建器。每轮修改后重新执行 Task 4 Step 6 和本 Task Step 1-3,直到所有 12 页合格。
|
||||
|
||||
- [ ] **Step 5: 提交 PPTX**
|
||||
|
||||
Run: `git add docs/finals/决赛答辩PPT.pptx`
|
||||
|
||||
Run: `git commit -m "docs: refresh finals defense deck"`
|
||||
|
||||
### Task 6: 跨文件一致性与最终验收
|
||||
|
||||
**Files:**
|
||||
- Create: `D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\verify_final_materials.py`
|
||||
- Verify: `docs/finals/决赛项目结项书.md`
|
||||
- Verify: `docs/finals/决赛项目结项书.docx`
|
||||
- Verify: `docs/finals/决赛答辩PPT.pptx`
|
||||
|
||||
**Interfaces:**
|
||||
- Consumes: Task 1-5 的事实清单和三份交付物。
|
||||
- Produces: 一致性报告、最终 Git 变更范围和可交付材料。
|
||||
|
||||
- [ ] **Step 1: 实现 OOXML 文本抽取与断言**
|
||||
|
||||
`verify_final_materials.py` 使用 `zipfile` 和 `xml.etree.ElementTree` 抽取 DOCX `word/document.xml` 以及 PPTX `ppt/slides/slide*.xml`/`ppt/notesSlides/notesSlide*.xml`。断言:
|
||||
|
||||
```python
|
||||
REQUIRED_ALL = [
|
||||
"[功能] 适配 offload_group_size 和 offload_num_in_group 到 Ascend NPU",
|
||||
"乘风破浪的大学生",
|
||||
"v0.18.0",
|
||||
]
|
||||
REQUIRED_REPORT_AND_DECK = [
|
||||
"72.2517%",
|
||||
"23.7374%",
|
||||
"0.434 GiB",
|
||||
"85.77",
|
||||
"v0.23.0rc1",
|
||||
]
|
||||
FORBIDDEN = [
|
||||
"51 / 1319",
|
||||
"0 / 198",
|
||||
"3.8666%",
|
||||
"当前实现优于 official latest",
|
||||
"7B 已通过 GSM8K",
|
||||
"7B 已通过 GPQA",
|
||||
]
|
||||
```
|
||||
|
||||
同时断言 PPTX 有 12 个 slide XML,备注中的建议时长之和为 680 秒。
|
||||
|
||||
- [ ] **Step 2: 运行跨文件校验器**
|
||||
|
||||
Run: `& 'C:\Users\30780\.cache\codex-runtimes\codex-primary-runtime\dependencies\python\python.exe' 'D:\PDSL\Ascend\.codex_tmp\finals-materials-refresh\verify_final_materials.py'`
|
||||
|
||||
Expected: 报告 Markdown/DOCX/PPTX 关键事实齐全、无禁止表述、PPT 12 页、建议时长 680 秒。
|
||||
|
||||
- [ ] **Step 3: 重跑与本次材料相关的仓库测试**
|
||||
|
||||
Run: `python -m unittest tests.model_executor.offloader.test_accuracy_repro tests.model_executor.offloader.test_parameter_optimization_analysis`
|
||||
|
||||
Run: `python -m unittest tests.model_executor.offloader.test_large_model_parameter_comparison`
|
||||
|
||||
Expected: 两组命令均返回 0,证明文档所引复现契约仍可用。
|
||||
|
||||
- [ ] **Step 4: 检查仓库差异和文件大小**
|
||||
|
||||
Run: `Get-Item docs/finals/决赛项目结项书.md,docs/finals/决赛项目结项书.docx,docs/finals/决赛答辩PPT.pptx | Select-Object Name,Length,LastWriteTime`
|
||||
|
||||
Run: `git diff --check`
|
||||
|
||||
Run: `git status --short --branch`
|
||||
|
||||
Expected: 三份交付物均非空;仅存在计划内的文档变更;核心代码和测试文件无变更。
|
||||
|
||||
- [ ] **Step 5: 记录最终视觉审查结果**
|
||||
|
||||
在 `visual-review.md` 写入 DOCX 页数、PPT 12 页、已查看的接触表、修复过的页面和剩余非阻断风险。不得使用“大概”、“应该”或“未检查”作为通过依据。
|
||||
|
||||
- [ ] **Step 6: 提交验收修复(仅在最终验收产生额外交付文件变更时)**
|
||||
|
||||
Run: `git add docs/finals/决赛项目结项书.md docs/finals/决赛项目结项书.docx docs/finals/决赛答辩PPT.pptx`
|
||||
|
||||
Run: `git commit -m "docs: finalize finals materials refresh"`
|
||||
|
||||
- [ ] **Step 7: 确认最终分支状态**
|
||||
|
||||
Run: `git status --short --branch`
|
||||
|
||||
Run: `git log --oneline -6`
|
||||
|
||||
Expected: 工作区干净,当前分支包含设计、计划和材料更新提交,但未自动推送。
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
# 决赛材料设计说明
|
||||
|
||||
## 1. 目标与交付物
|
||||
|
||||
本次工作基于分支 `feature/offload-group-num-v018-optimized` 的当前代码、初赛结项材料及已有测试结果,形成一套可用于决赛提交与 12 分钟现场答辩的正式材料。
|
||||
|
||||
最终交付物统一放在 `docs/finals/`:
|
||||
|
||||
1. `决赛项目结项书.md`:便于代码仓审阅和版本追踪。
|
||||
2. `决赛项目结项书.docx`:正式排版版本,包含封面、目录、正文、表格、页眉页脚和页码。
|
||||
3. `决赛答辩PPT.pptx`:继承赛事提供的 PowerPoint 模板,用于 12 分钟答辩。
|
||||
|
||||
项目主标题沿用:
|
||||
|
||||
> [功能] 适配 offload_group_size 和 offload_num_in_group 到 Ascend NPU
|
||||
|
||||
副标题使用:
|
||||
|
||||
> vLLM Ascend 权重分组预取卸载参数适配与优化
|
||||
|
||||
团队名称使用“乘风破浪的大学生”。
|
||||
|
||||
## 2. 内容事实与证据口径
|
||||
|
||||
材料只使用仓库中能够复核的代码、脚本和测试结果,不虚构远端性能数据或主仓对比结论。
|
||||
|
||||
关键环境口径:
|
||||
|
||||
- vLLM Ascend:v0.18.0 基线,提交 `72dc68973bd7e6ceef9a122de316a7ee9c9a84aa`
|
||||
- 当前优化提交:`f0ab80524e2088763ab1e9859534f05912850194`
|
||||
- 模型:`Qwen2.5-1.5B-Instruct`
|
||||
- CANN:8.5.1
|
||||
- PyTorch:2.9.0+cpu
|
||||
- torch-npu:2.9.0
|
||||
- triton-ascend:3.2.0.dev20260322
|
||||
|
||||
验证结果口径:
|
||||
|
||||
- 本地验收:35/35
|
||||
- 远端 offloader 测试:69 passed,21 subtests passed
|
||||
- GSM8K:baseline、group_8_num_1、group_8_num_2 均为 51/1319,精度差异为 0
|
||||
- GPQA Diamond:三组均为 0/198,输出哈希一致;必须明确这是小模型零样本基线表现,不表述为高精度
|
||||
- 性能中位数:
|
||||
- baseline:742.77 tok/s
|
||||
- group_8_num_1:678.85 tok/s,节省约 0.2615 GB 权重常驻空间
|
||||
- group_8_num_2:383.49 tok/s,节省约 0.5230 GB 权重常驻空间
|
||||
|
||||
性能结论表述为“可配置的显存容量与吞吐权衡”,不将吞吐下降包装为性能提升。
|
||||
|
||||
## 3. 结项书结构
|
||||
|
||||
结项书沿用初赛项目结项书的七章结构,并补充决赛阶段优化、主仓版本背景和完整复现方式:
|
||||
|
||||
1. 项目概述
|
||||
2. 参数理解与技术难点
|
||||
3. 技术方案
|
||||
4. 实际交付
|
||||
5. 测试结论
|
||||
6. AI 工具使用经验
|
||||
7. 限制与后续工作
|
||||
|
||||
正文重点说明:
|
||||
|
||||
- 两个参数的语义、约束和适用场景
|
||||
- Ascend `PrefetchOffloader` 的静态缓冲、NPU Stream/Event 和分组预取机制
|
||||
- 对 vLLM 官方 `auto/prefetch/uva` 后端选择方式的兼容
|
||||
- pinned CPU storage、stride/dtype 保持、设备映射和健康检查
|
||||
- 决赛相对初赛的工程化增强
|
||||
- GSM8K、GPQA 和性能测试的一键复现命令、输出路径与判定标准
|
||||
- v0.18.0 目标主仓未包含 Ascend 专用实现,因此“必须优于主仓已有实现”的附加条件不适用于本目标基线
|
||||
|
||||
Word 版本采用正式技术报告风格,以清晰层级、稳定表格和可打印性为主,不使用装饰性过强的版式。
|
||||
|
||||
## 4. PPT 结构与节奏
|
||||
|
||||
PPT 使用赛事模板的 12 页结构,不额外增加页面:
|
||||
|
||||
1. 封面
|
||||
2. 决赛成果总览
|
||||
3. 参数语义与收益边界
|
||||
4. Ascend 分组预取卸载架构
|
||||
5. 项目概述与版本背景
|
||||
6. 设计实现
|
||||
7. 功能与工程验证
|
||||
8. GSM8K、GPQA 精度测试
|
||||
9. 性能与显存权衡
|
||||
10. AI 辅助参与与效果
|
||||
11. 创新点与决赛阶段提升
|
||||
12. 总结与致谢
|
||||
|
||||
正式讲述目标为 11 分 20 秒,预留约 40 秒缓冲。第 6、9、11 页是答辩重点,总计约 4 分 40 秒。
|
||||
|
||||
每页采用“一个结论 + 一组证据”的表达方式,避免将结项书段落直接搬进幻灯片。图表优先展示参数关系、数据流、测试矩阵和量化结果。
|
||||
|
||||
## 5. 模板继承规则
|
||||
|
||||
- 保留赛事模板的母版、页面比例、标题体系、页脚和品牌视觉。
|
||||
- 模板中的示意页只作为布局参考,清除“请勿直接填写”等提示文字。
|
||||
- 不改变封面和结束页的核心视觉识别。
|
||||
- 新增内容必须落在模板既有内容区域内,不遮挡标题、页码或赛事标识。
|
||||
- 性能和精度数据采用同一套数字格式,图例能够区分 baseline、group_8_num_1 和 group_8_num_2。
|
||||
- 在 PowerPoint 备注中记录每页证据来源和建议讲述要点。
|
||||
|
||||
## 6. 验收标准
|
||||
|
||||
### 内容验收
|
||||
|
||||
- 三份交付物的标题、团队名称、版本和测试数字完全一致。
|
||||
- 两个参数均有实现说明、精度验证、性能验证和复现脚本说明。
|
||||
- 明确解释 GPQA 结果为 0 的背景,并保留输出一致性证据。
|
||||
- 明确解释显存收益与吞吐损失,不作超出证据的结论。
|
||||
|
||||
### 文档验收
|
||||
|
||||
- Markdown 链接和代码路径有效。
|
||||
- Word 文档可正常打开,目录、表格、分页、页眉页脚和中文字体显示正常。
|
||||
- Word 文档经过逐页 PNG 渲染检查,无截断、重叠、孤立标题或空白异常。
|
||||
|
||||
### PPT 验收
|
||||
|
||||
- 12 页完整,适配 12 分钟答辩。
|
||||
- 继承模板母版及主要视觉语言。
|
||||
- 所有页面经过桌面比例渲染检查,无文本溢出、遮挡或错位。
|
||||
- 通过幻灯片结构检查和模板一致性检查。
|
||||
|
||||
### 仓库验收
|
||||
|
||||
- 最终文件位于 `docs/finals/`。
|
||||
- 核心代码和已有测试结果不被改写。
|
||||
- 制作脚本与内部渲染中间件不作为决赛交付物放入 `docs/finals/`。
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# GSM8K 与 GPQA 单一精度协议修复设计
|
||||
|
||||
## 背景
|
||||
|
||||
当前精度脚本把原始题目字符串直接传给 `Qwen2.5-1.5B-Instruct`,没有应用模型
|
||||
chat template。GSM8K 默认只生成 128 tokens,正式结果中 1304/1319 个输出达到
|
||||
长度上限;GPQA 默认生成 16 tokens,但评分器要求整段输出只能是单独的
|
||||
`A/B/C/D`,导致所有带解释的答案均被判为无法解析。
|
||||
|
||||
本次只保留一套正式成绩,不输出兼容成绩或实验性第二成绩。修复目标是建立一个
|
||||
确定性、可复现、对 baseline 与两个 offload case 完全一致的测试协议。
|
||||
|
||||
## 测试协议
|
||||
|
||||
协议名称为 `qwen-chat-v1`,使用以下固定设置:
|
||||
|
||||
| 项目 | GSM8K | GPQA Diamond |
|
||||
|---|---|---|
|
||||
| 模型输入 | Qwen chat template | Qwen chat template |
|
||||
| temperature | 0 | 0 |
|
||||
| max tokens | 512 | 8 |
|
||||
|回答要求 | 分步计算,末行 `The answer is <number>.` | 只返回一个大写选项字母 |
|
||||
|评分 | 最终规范化数值精确匹配 | 输出开头的明确 `A/B/C/D` |
|
||||
|
||||
GSM8K 的 system message 要求模型成为精确的数学问题求解器;user message 包含
|
||||
原始问题,并明确要求最后一行使用固定答案格式,且其后不再输出内容。
|
||||
|
||||
GPQA 的 system message 要求模型完成单项选择;user message 包含稳定种子打乱后的
|
||||
题目和四个选项,并要求只输出 `A`、`B`、`C` 或 `D`,不得附加解释。
|
||||
|
||||
## 代码结构
|
||||
|
||||
`scripts/accuracy_gsm8k_gpqa.py` 增加三个清晰边界:
|
||||
|
||||
1. `prompt_for_example` 只负责构造数据集相关的 user message 和 gold answer。
|
||||
2. 新的消息构造函数为每个数据集附加固定 system message。
|
||||
3. `generate_prompts` 从 `LLM` 获取 tokenizer,调用
|
||||
`apply_chat_template(..., tokenize=False, add_generation_prompt=True)` 后再生成。
|
||||
|
||||
GPQA 解析器改为只解析输出开头的明确答案。它接受 `C`、`C. explanation`、
|
||||
`Answer: C` 和 `(C)`,但继续拒绝 `I considered C` 等正文中的偶然字母,避免通过
|
||||
搜索任意位置的选项字母虚增分数。
|
||||
|
||||
运行脚本将 GSM8K 和 GPQA 的默认生成预算分别改为 512 和 8。Python CLI 的通用
|
||||
默认值改为 512;正式 shell 入口仍按数据集显式传值。
|
||||
|
||||
## 结果与诊断
|
||||
|
||||
每条 example JSONL 除现有的题目、gold、prediction 和 correct 外,增加:
|
||||
|
||||
- `output_token_count`
|
||||
- `finish_reason`
|
||||
- `parsed_answer`
|
||||
|
||||
结果 JSON 增加 `prompt_protocol` 和生成结束原因汇总。该信息用于证明正式结果没有
|
||||
再次因 token 上限大面积截断,也让评审能够审计 GPQA 的实际解析结果。
|
||||
|
||||
所有三组参数 case 必须使用相同协议、数据集哈希、随机种子和采样设置。
|
||||
`verify_accuracy_matrix.py` 继续以 baseline 为参照验证两个 offload case 没有精度下降。
|
||||
|
||||
## 错误处理
|
||||
|
||||
- tokenizer 不支持 chat template 时立即报错,不回退到原始文本协议。
|
||||
- vLLM 没有返回候选结果时保存空预测、0 token 和 `missing` 结束原因。
|
||||
- GPQA 无法从输出开头解析明确选项时记录 `parsed_answer: null` 并判错。
|
||||
- 参数合法性和本地数据集 manifest 校验保持不变。
|
||||
|
||||
## 测试策略
|
||||
|
||||
按 TDD 增加以下回归测试,并在实现前确认失败:
|
||||
|
||||
1. GSM8K 与 GPQA 构造固定、数据集相关的提示内容。
|
||||
2. 所有生成输入均经过 tokenizer chat template。
|
||||
3. GPQA 接受开头明确答案及其后解释,但拒绝正文中的偶然选项字母。
|
||||
4. generation record 正确保存文本、token 数和结束原因。
|
||||
5. 结果元数据包含 `qwen-chat-v1` 协议与结束原因汇总。
|
||||
6. shell 正式入口默认使用 GSM8K 512、GPQA 8 tokens。
|
||||
|
||||
本地单测通过后,在 Ascend 服务器依次执行:
|
||||
|
||||
1. 精度脚本单测。
|
||||
2. 每个数据集两题的 baseline 冒烟测试。
|
||||
3. GSM8K 1319 题和 GPQA Diamond 198 题的三组全量矩阵。
|
||||
4. 校验三组结果的样本数、数据集哈希、协议元数据和相对精度差。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- GSM8K 与 GPQA 都只产生一套正式成绩。
|
||||
- 正式输入全部应用 Qwen chat template。
|
||||
- GSM8K 不再出现接近全量样本的长度截断。
|
||||
- GPQA 可解析率不再因带解释输出而人为变成 0。
|
||||
- baseline、`group_8_num_1`、`group_8_num_2` 使用完全相同的测试协议。
|
||||
- 两个 offload case 相对 baseline 的允许精度下降仍为 0。
|
||||
- 单测、冒烟测试和全量矩阵均保留可复现日志与 JSON 结果。
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
# Qwen2.5-7B Current vs Latest Parameter Performance Design
|
||||
|
||||
## Goal
|
||||
|
||||
Measure the parameter-performance trade-off of the current offloader
|
||||
implementation against the latest official vLLM Ascend implementation with a
|
||||
larger model, while separating offloader implementation effects from complete
|
||||
runtime-version effects.
|
||||
|
||||
The experiment must answer:
|
||||
|
||||
1. How do `offload_group_size` and `offload_num_in_group` affect throughput,
|
||||
latency, load time, and KV-cache capacity on Qwen2.5-7B-Instruct?
|
||||
2. At identical parameter values and on an identical v0.23 runtime, is the
|
||||
current implementation measurably faster than the latest official
|
||||
implementation?
|
||||
3. How does the current native v0.18 deployment compare with the latest native
|
||||
v0.23 deployment at the practical low-density parameter settings?
|
||||
|
||||
Accuracy testing and further offloader implementation changes are outside this
|
||||
experiment's scope.
|
||||
|
||||
## Fixed Inputs
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Model | Qwen2.5-7B-Instruct |
|
||||
| Model layers | 28 |
|
||||
| Hardware | One Ascend 910B2C, physical device 6 / logical device 0 |
|
||||
| Current target | vLLM Ascend v0.18.0-compatible project implementation |
|
||||
| Latest target | vLLM Ascend v0.23.0rc1, commit `f4a08bd` |
|
||||
| Latest vLLM | 0.23.0 |
|
||||
| CANN | 9.0.1 |
|
||||
| dtype | FP16 |
|
||||
| Execution mode | eager |
|
||||
| Prompt count | 8 |
|
||||
| Input tokens per prompt | 128 |
|
||||
| Maximum output tokens per prompt | 64 |
|
||||
| Maximum model length | 512 |
|
||||
| GPU memory utilization | 0.5 |
|
||||
| Warm-up rounds | 1 |
|
||||
| Random seed | 0 |
|
||||
|
||||
The model revision and every downloaded model file will be recorded in the
|
||||
environment manifest and SHA-256 manifest.
|
||||
|
||||
## Comparison Architecture
|
||||
|
||||
### Layer A: Same-Runtime Implementation Comparison
|
||||
|
||||
This is the primary comparison.
|
||||
|
||||
Both implementations use the official v0.23.0rc1 runner, vLLM 0.23.0, Python,
|
||||
PyTorch, torch-npu, CANN, model files, benchmark code, and process environment.
|
||||
|
||||
- The latest cases use the unmodified official `NPUPrefetchOffloader`.
|
||||
- The current cases copy only the project
|
||||
`vllm_ascend/model_executor/offloader/` package into a temporary overlay.
|
||||
- The overlay adds only the compatibility alias
|
||||
`NPUPrefetchOffloader = AscendPrefetchOffloader` required by the v0.23 runner.
|
||||
- The overlay must not alter selection, transfers, buffering, synchronization,
|
||||
or benchmark behavior.
|
||||
|
||||
Logs must prove which implementation class was loaded for every offload run.
|
||||
|
||||
### Layer B: Native-Stack Confirmation
|
||||
|
||||
This is the secondary deployment comparison.
|
||||
|
||||
The current implementation runs natively in the prepared v0.18 environment.
|
||||
Its results are compared with the official v0.23 native results from Layer A
|
||||
for the same model, workload, and parameters.
|
||||
|
||||
Because vLLM, Python, PyTorch, and plugin versions differ, Layer B is reported
|
||||
as an end-to-end stack comparison and is not used to attribute a difference
|
||||
solely to the offloader.
|
||||
|
||||
## Parameter Matrix
|
||||
|
||||
The full same-runtime matrix uses these 11 parameter points:
|
||||
|
||||
| Parameters `(group_size,num_in_group)` | Selected-layer count | Purpose |
|
||||
|---|---:|---|
|
||||
| `(0,1)` | 0 | No-offload baseline |
|
||||
| `(28,1)` | 1 | Minimum offload density |
|
||||
| `(14,1)` | 2 | Two spaced layers |
|
||||
| `(28,2)` | 2 | Two adjacent layers |
|
||||
| `(8,1)` | 3 | Previous competition parameter |
|
||||
| `(7,1)` | 4 | Four spaced layers |
|
||||
| `(14,2)` | 4 | Four adjacent pairs |
|
||||
| `(8,2)` | 6 | Previous competition parameter |
|
||||
| `(4,1)` | 7 | Medium-high density |
|
||||
| `(2,1)` | 14 | Alternating layers |
|
||||
| `(4,2)` | 14 | Adjacent layer pairs |
|
||||
|
||||
The no-offload baseline is shared. Each of the ten positive-group points runs
|
||||
once with each implementation.
|
||||
|
||||
### Main Matrix
|
||||
|
||||
- 21 labels: one shared baseline plus ten current and ten latest cases.
|
||||
- Three independent processes per label.
|
||||
- Three measured rounds per process.
|
||||
- 63 independent processes in total.
|
||||
- Cases are interleaved and the starting position rotates between repeats.
|
||||
- A fresh process loads the model for every run.
|
||||
- A three-second cooldown separates processes.
|
||||
|
||||
### Long-Window Confirmation
|
||||
|
||||
The low-density candidates receive additional confirmation:
|
||||
|
||||
- Shared baseline.
|
||||
- Current and latest `(28,1)`.
|
||||
- Current and latest `(14,1)`.
|
||||
- Three independent processes per label.
|
||||
- Ten measured rounds per process.
|
||||
- 15 independent processes and 30 measured rounds per label.
|
||||
|
||||
### Native v0.18 Confirmation
|
||||
|
||||
The current native v0.18 stack runs:
|
||||
|
||||
- `(0,1)`, `(28,1)`, `(14,1)`, and `(8,1)`.
|
||||
- Three independent processes per point.
|
||||
- Three measured rounds per process.
|
||||
- 12 independent processes in total.
|
||||
|
||||
The complete design therefore contains 90 independent benchmark processes.
|
||||
|
||||
## Metrics and Aggregation
|
||||
|
||||
The primary metric is median per-process output throughput in tokens per
|
||||
second. Each process contributes one aggregate throughput value.
|
||||
|
||||
Supporting metrics are:
|
||||
|
||||
- Pooled measured-round throughput median, mean, and sample standard deviation.
|
||||
- P50 and P95 generation latency.
|
||||
- Model load time.
|
||||
- Available KV-cache memory reported by the runtime.
|
||||
- Selected layers and offloaded layer count.
|
||||
- Weight-storage capacity released by the current implementation.
|
||||
- Process RSS and NPU HBM samples from the common benchmark harness.
|
||||
|
||||
For each implementation, the report computes a capacity-throughput Pareto
|
||||
frontier. Equal-capacity layouts are compared directly.
|
||||
|
||||
The long-window comparison is used to decide whether low-density differences
|
||||
are larger than runtime noise. Small positive deltas below observed variation
|
||||
must be described as equivalent performance, not as a proven speedup.
|
||||
|
||||
## Model Acquisition and Smoke Gates
|
||||
|
||||
Qwen2.5-7B-Instruct is downloaded to
|
||||
`/data/models/Qwen2.5-7B-Instruct`. Before the formal matrix:
|
||||
|
||||
1. Confirm at least 14 GiB free disk space remains after download.
|
||||
2. Record model file sizes and SHA-256 hashes.
|
||||
3. Load the baseline once in the v0.18 native environment.
|
||||
4. Load the baseline once in the v0.23 native environment.
|
||||
5. Run one current-overlay and one latest-official offload smoke case.
|
||||
6. Confirm the NPU is healthy and no stale process is using the card.
|
||||
|
||||
The formal matrix does not start if either runtime cannot load the same model,
|
||||
if a smoke case raises a traceback, or if implementation-class validation
|
||||
fails.
|
||||
|
||||
## Failure Handling
|
||||
|
||||
- Each benchmark process receives a bounded timeout.
|
||||
- On timeout, OOM, NPU health failure, or traceback, stop the matrix and retain
|
||||
the completed logs.
|
||||
- Do not silently skip a parameter point.
|
||||
- If a high-density point is valid but exceptionally slow, retain it and report
|
||||
the measured cost.
|
||||
- If custom-operator registration is degraded, the comparison may proceed only
|
||||
when every same-runtime case has the same warning state. The report must
|
||||
disclose the degradation and avoid cross-environment absolute comparisons.
|
||||
|
||||
## Validation
|
||||
|
||||
The result validator must assert:
|
||||
|
||||
- Exact run and log counts for all three phases.
|
||||
- Exactly one `BENCH_RESULT` per log.
|
||||
- Zero Python tracebacks.
|
||||
- Expected current and official implementation classes in same-runtime logs.
|
||||
- Matching model path and workload values in every JSONL record.
|
||||
- Expected offload parameters and selected-layer counts.
|
||||
- NPU health before and after the experiment.
|
||||
|
||||
The project offloader tests, analyzer unit tests, shell syntax checks, and local
|
||||
post-download result recomputation must pass before completion is reported.
|
||||
|
||||
## Artifacts
|
||||
|
||||
Repository artifacts:
|
||||
|
||||
- `scripts/run_large_model_parameter_comparison.sh`
|
||||
- `scripts/analyze_large_model_parameter_comparison.py`
|
||||
- Analyzer tests under `tests/model_executor/offloader/`
|
||||
- `docs/finals/qwen25-7b-current-vs-latest-parameter-performance.md`
|
||||
|
||||
Raw artifacts:
|
||||
|
||||
- Environment and model manifests.
|
||||
- Main, long-window, and native-v0.18 JSONL files.
|
||||
- One log per benchmark process.
|
||||
- JSON, CSV, and Markdown summaries.
|
||||
- Validation output.
|
||||
- Test logs.
|
||||
- SHA-256 manifest and compressed archive.
|
||||
|
||||
The raw result directory and archive are downloaded under `D:\PDSL\Ascend`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
The task is complete when:
|
||||
|
||||
1. Qwen2.5-7B-Instruct is tested successfully in both runtime versions.
|
||||
2. All 90 planned processes complete or a documented hardware/runtime blocker
|
||||
prevents a specific phase.
|
||||
3. Same-runtime and native-stack results are reported separately.
|
||||
4. The report identifies the practical parameter recommendation and the
|
||||
capacity-performance knee.
|
||||
5. Any claimed improvement over the latest implementation is supported by a
|
||||
repeatable delta larger than observed run-to-run variation.
|
||||
6. Raw logs, reproduction scripts, summaries, and hashes are available locally.
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# 决赛结项材料更新设计说明
|
||||
|
||||
## 1. 目标与范围
|
||||
|
||||
本次更新基于远端分支 `feature/offload-group-num-v018-optimized` 的已同步提交 `851feb5`,将 2026 年 7 月 29 日版决赛结项书和答辩 PPT 更新为反映完整决赛开发历史、精度验证与 Qwen2.5-7B 最终性能对比的正式材料。
|
||||
|
||||
交付物保持在 `docs/finals/`:
|
||||
|
||||
1. `决赛项目结项书.md`:内容权威源和可版本追踪文本。
|
||||
2. `决赛项目结项书.docx`:正式排版和可打印版本。
|
||||
3. `决赛答辩PPT.pptx`:沿用赛事模板的 12 页、12 分钟答辩材料。
|
||||
|
||||
不修改核心实现、测试脚本或原始实验证据。不将工作区外的大型结果归档加入 Git。
|
||||
|
||||
## 2. 统一事实与证据口径
|
||||
|
||||
材料采用“双证据主线”,不把不同模型的精度和性能数据混为同一次实验。
|
||||
|
||||
### 2.1 精度证据
|
||||
|
||||
- 模型:`Qwen2.5-1.5B-Instruct`。
|
||||
- 数据集:GSM8K test 1319 条,GPQA Diamond 198 条。
|
||||
- 参数组:`baseline_0_1`、`group_8_num_1`、`group_8_num_2`。
|
||||
- GSM8K:三组均为 953/1319,即 72.2517%。
|
||||
- GPQA Diamond:三组均为 47/198,即 23.7374%。
|
||||
- 结论:两个参数 case 相对 baseline 的精度下降为 0,且同数据集的逐题输出 SHA-256 一致。该结论只证明 offload 未引入额外精度损失,不表示模型的绝对任务精度达到其他模型水平。
|
||||
|
||||
### 2.2 性能证据
|
||||
|
||||
- 模型:`Qwen/Qwen2.5-7B-Instruct`,28 个 decoder layers,benchmark 转为 `float16`。
|
||||
- 硬件:Ascend 910B2C,逻辑卡 0,物理 NPU 6。
|
||||
- 运行栈:CANN 9.0.1;同运行时主对比基于 vLLM Ascend v0.23.0rc1(`f4a08bd`)。
|
||||
- 当前实现性能证据所用项目部署提交:`ec673f8`;材料更新分支 HEAD:`851feb5`。
|
||||
- 证据规模:63 个同运行时进程、15 个长窗口进程、12 个 native v0.18 进程,共 90 条记录、90 份日志和 90 份运行时 offloader 证据。
|
||||
- 最终归档:`qwen25-7b-current-vs-latest-20260731-hardened-v6-ec673f8-results.tar.gz`,SHA-256 为 `a8cc0ae4aa506d5c75f97d891001006889554e6b4b14603758e0a5e2014521f3`。
|
||||
- 主结论:当前实现相对 official latest 未建立可测量、稳定的速度优势。同运行时 10 组正参数对的中位数差范围为 -0.086595% 至 +0.043851%。
|
||||
- 推荐点:`(28,1)` 释放约 0.434 GiB 权重容量,KV cache 从 15.66 GiB 增加至 16.09 GiB,吞吐保留约 85.77%,是本负载的实际容量膝点。
|
||||
- 限制:7B 运行未采集精度,不从该实验推导 accuracy 结论。
|
||||
|
||||
### 2.3 测试与验证证据
|
||||
|
||||
- hardened-v6 远端明确列出的 8 个非 Windows offloader 测试文件:87 passed in 18.89s。
|
||||
- hardened-v6 本地 focused suite:41 tests passed。
|
||||
- 最终归档 `validation.json`:`valid: true`、`model_files_verified: true`,63 + 15 + 12 条记录与日志计数一致。
|
||||
- 远端整目录诊断中的 15 个失败均来自 Windows 本地路径或打包流程断言,不计为 Linux/Ascend 运行时失败。
|
||||
|
||||
## 3. 结项书设计
|
||||
|
||||
结项书沿用原七章结构,避免为追加新实验而破坏初赛到决赛的文档延续性:
|
||||
|
||||
1. 项目概述:更新版本、提交、决赛阶段定位和 latest 对比结论。
|
||||
2. 参数理解与技术难点:保留参数语义、边界、存储布局和 NPU 异步依赖。
|
||||
3. 技术方案:保留 selection/prefetch/runner 架构,增加运行时证据 hook 和 fail-closed 身份绑定。
|
||||
4. 实际交付:补充大模型对比 runner、analyzer、独立校验和报告路径。
|
||||
5. 测试结论:先给出 1.5B 精度,再给出 7B 容量/吞吐与 current/latest 结论,并明确实验不同源。
|
||||
6. AI 工具使用经验:从“生成脚本”扩展至证据身份、失败关闭和独立重算审计。
|
||||
7. 限制与后续工作:明确无速度领先结论、7B 无精度数据、单卡固定负载以及大型原始归档位于 Git 之外。
|
||||
|
||||
Markdown 作为三份材料的内容权威源。DOCX 从更新后的 Markdown 重新生成,不直接在旧 DOCX 中手工修改数字。
|
||||
|
||||
## 4. PPT 设计与答辩节奏
|
||||
|
||||
PPT 继续使用赛事模板、12 页和原标题,副标题保持“vLLM Ascend 权重分组预取卸载参数适配与优化”。页面结构为:
|
||||
|
||||
1. 封面。
|
||||
2. 决赛成果总览:2 个参数、2 个精度数据集、90 个 7B 实验进程、1 套完整复现链。
|
||||
3. 参数语义与容量边界。
|
||||
4. Ascend 分组预取卸载架构。
|
||||
5. 开发历程与版本对比:v0.18.0 适配、精度协议修复、7B 公平对比、证据加固。
|
||||
6. 设计实现:selection、storage、stream/event 和 runner 契约。
|
||||
7. 功能与证据验证:87 项 Ascend 有关测试、41 项本地 focused tests、90 份 runtime evidence。
|
||||
8. GSM8K / GPQA 精度:展示 72.2517% / 23.7374% 与三组输出一致。
|
||||
9. Qwen2.5-7B 容量与吞吐权衡:以 baseline、`(28,1)`、`(14,1)` 为主视图。
|
||||
10. 当前实现 vs official latest:明确无可确认速度优势,展示同运行时和长窗口结论。
|
||||
11. 决赛阶段提升与工程价值:回溯适配、官方契约、证据身份和可复现性。
|
||||
12. 总结与致谢。
|
||||
|
||||
建议讲述总时长约 11 分 20 秒,预留约 40 秒。性能主结论不表述为“当前实现优于 latest”,而表述为“在 v0.18.0 上完成参数能力回溯,并建立与后续官方实现的可复现公平对比”。
|
||||
|
||||
## 5. 文档与演示文件生成
|
||||
|
||||
- DOCX 保持 A4 正式技术报告风格,含封面、目录、页眉页脚、页码、表格和代码块。
|
||||
- PPT 保留原模板主题、比例、封面和结束页品牌识别;只替换内容区与讲者备注。
|
||||
- 表格和图表的数据从统一事实源生成,避免 Markdown、DOCX 与 PPT 之间的数字漂移。
|
||||
- 内部构建脚本和渲染中间件位于 `.codex_tmp/finals-materials-refresh/`,不作为决赛交付上传。
|
||||
|
||||
## 6. 验收与风险控制
|
||||
|
||||
### 6.1 内容验收
|
||||
|
||||
- 三份材料的主标题、副标题、团队、目标版本和关键结论一致。
|
||||
- 性能与精度段落每处都明确标注模型和实验协议。
|
||||
- 不出现“当前实现速度优于 official latest”或“7B 已通过 GSM8K/GPQA”等超出证据的表述。
|
||||
- 归档 SHA-256、样本数、准确率、进程数和主要吞吐数据均与仓库报告或本地最终归档一致。
|
||||
|
||||
### 6.2 文档验收
|
||||
|
||||
- Markdown 通过差异格式和占位符扫描。
|
||||
- DOCX 经过结构检查、全页渲染和逐页视觉检查,无截断、重叠、孤立标题或异常空白页。
|
||||
- PPT 保持 12 页,经过全页渲染、边界/溢出检查和视觉审查,不遮挡模板标识、页码或关键数据。
|
||||
- 从 DOCX/PPTX 提取文本,与 Markdown 交叉校验标题、版本和关键数字。
|
||||
|
||||
### 6.3 仓库验收
|
||||
|
||||
- 完成后仅包含设计、计划和三份交付材料的预期变更。
|
||||
- 核心代码、测试脚本和原始实验证据保持不变。
|
||||
- 不在未经用户明确要求时自动推送最终材料。
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
# vLLM Ascend v0.18.0 Deployment and Reproduction
|
||||
|
||||
## Scope
|
||||
|
||||
This submission adapts and tests these two parameters on vLLM Ascend v0.18.0:
|
||||
|
||||
- `--offload-group-size`
|
||||
- `--offload-num-in-group`
|
||||
|
||||
The comparison matrix is fixed to three cases:
|
||||
|
||||
| Case | group size | num in group |
|
||||
| --- | ---: | ---: |
|
||||
| `baseline_0_1` | 0 | 1 |
|
||||
| `group_8_num_1` | 8 | 1 |
|
||||
| `group_8_num_2` | 8 | 2 |
|
||||
|
||||
The v0.18.0 base scan did not find an existing implementation of these parameters, so the additional requirement to outperform an upstream implementation is not applicable. Performance scripts still quantify throughput, latency, process RSS, and NPU HBM trade-offs against the baseline.
|
||||
|
||||
## Verified Software Matrix
|
||||
|
||||
| Component | Version |
|
||||
| --- | --- |
|
||||
| vLLM | `0.18.0+empty` |
|
||||
| vLLM Ascend source base | `72dc68973bd7e6ceef9a122de316a7ee9c9a84aa` |
|
||||
| CANN | `8.5.1` |
|
||||
| torch | `2.9.0+cpu` |
|
||||
| torch-npu | `2.9.0` |
|
||||
| triton-ascend | `3.2.0.dev20260322` |
|
||||
| Python | `3.11` |
|
||||
|
||||
The performance evidence below was measured in the CANN 8.5.1 environment.
|
||||
After server migration, the corrected `qwen-chat-v1` accuracy matrix was rerun
|
||||
on CANN 9.0.0 with Python 3.11.6, vLLM 0.18.0+empty,
|
||||
vLLM Ascend `0.1.dev1+g72dc68973.d20260729`, torch 2.9.0+cpu,
|
||||
torch-npu 2.9.0.post2, and triton-ascend 3.2.1. All three accuracy cases in
|
||||
that matrix used the same migrated runtime; no cross-CANN performance
|
||||
conclusion is inferred from those results.
|
||||
|
||||
Remote paths:
|
||||
|
||||
```text
|
||||
/data/vllm-ascend-v0.18.0
|
||||
/data/vllm-source-v0.18.0
|
||||
/data/vllm-ascend-v0.18.0/.venv
|
||||
/usr/local/Ascend/cann-8.5.1/cann-8.5.1/set_env.sh
|
||||
/usr/local/Ascend/cann-9.0.0/set_env.sh
|
||||
/data/models/Qwen2.5-1.5B-Instruct
|
||||
```
|
||||
|
||||
## Runtime Installation
|
||||
|
||||
Install the CANN 8.5.1 toolkit, 910B operator package, and NNAL package first. Then rebuild every CANN-bound plugin artifact:
|
||||
|
||||
```bash
|
||||
cd /data/vllm-ascend-v0.18.0
|
||||
bash scripts/install_v018_runtime_compat.sh
|
||||
```
|
||||
|
||||
The installer pins the Python packages, downloads the exact Triton wheel, clears only generated build artifacts under the guarded repository path, and rebuilds the editable vLLM Ascend package.
|
||||
|
||||
## Device Mapping and Health
|
||||
|
||||
The runtime uses logical device 0. The reproduction scripts derive the physical
|
||||
NPU index from `npu-smi info -m` before running the health query. After the
|
||||
cloud instance restart, logical device 0 maps to physical NPU 0/chip 0 and the
|
||||
device reports `Health Status : OK`:
|
||||
|
||||
```bash
|
||||
export ASCEND_RT_VISIBLE_DEVICES=0
|
||||
bash scripts/check_npu_health.sh
|
||||
```
|
||||
|
||||
Set `NPU_INDEX` and `NPU_CHIP_INDEX` only when an explicit override is needed.
|
||||
Do not bypass the health check for formal runs. `SKIP_NPU_HEALTH_CHECK=1`
|
||||
exists only for environments whose driver does not implement the health query.
|
||||
|
||||
## Dataset Evidence
|
||||
|
||||
Materialized datasets:
|
||||
|
||||
| Dataset | Rows | SHA-256 |
|
||||
| --- | ---: | --- |
|
||||
| GSM8K test | 1319 | `32c548f08195e19e33408b844dd7be6aa4bcae457d957bc05909ff4bd4a00595` |
|
||||
| GPQA Diamond | 198 | `0c16aaa98af019ca691f98eeec9f99ca4cabfb9c920c9e19e40ebedf8d6a0174` |
|
||||
|
||||
Files are under `/data/accuracy-gsm8k-gpqa`. Each accuracy result records the dataset source, revision, row count, and hash.
|
||||
|
||||
## Performance Reproduction
|
||||
|
||||
```bash
|
||||
cd /data/vllm-ascend-v0.18.0
|
||||
MODEL_PATH=/data/models/Qwen2.5-1.5B-Instruct \
|
||||
RESULTS_DIR=/data/v018-performance-results \
|
||||
REPEATS=3 WARMUP_ROUNDS=1 MEASURED_ROUNDS=3 \
|
||||
PROMPT_COUNT=8 INPUT_TOKENS=128 MAX_TOKENS=64 MAX_MODEL_LEN=512 \
|
||||
bash scripts/run_benchmark_all.sh
|
||||
```
|
||||
|
||||
Artifacts:
|
||||
|
||||
```text
|
||||
performance-runs.jsonl
|
||||
performance-summary.json
|
||||
performance-summary.csv
|
||||
performance-summary.md
|
||||
performance-run.log
|
||||
```
|
||||
|
||||
### Formal performance result
|
||||
|
||||
The formal run at `/data/v018-performance-formal-20260721` contains nine raw
|
||||
runs: three independent model loads for each case. The table reports medians.
|
||||
|
||||
| Case | Selected layers | Weight storage saved | KV cache available | Output tok/s | Change | P50 round latency |
|
||||
| --- | --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| `baseline_0_1` | none | 0.0000 GB | 27.27 GiB | 742.77 | 0.00% | 0.6927 s |
|
||||
| `group_8_num_1` | 7, 15, 23 | 0.2615 GB | 27.53 GiB | 678.85 | -8.61% | 0.7501 s |
|
||||
| `group_8_num_2` | 6, 7, 14, 15, 22, 23 | 0.5230 GB | 27.79 GiB | 383.49 | -48.37% | 1.3351 s |
|
||||
|
||||
The two parameters provide a quantifiable memory/throughput trade-off. Do not
|
||||
interpret the post-generation `npu-smi` delta as weight storage: vLLM consumes
|
||||
newly available HBM for a larger KV cache. The model-loader logs provide the
|
||||
weight-storage and KV-capacity evidence shown above.
|
||||
|
||||
## Accuracy Reproduction
|
||||
|
||||
Full GSM8K and GPQA Diamond:
|
||||
|
||||
```bash
|
||||
cd /data/vllm-ascend-v0.18.0
|
||||
CANN_ENV=/usr/local/Ascend/cann-9.0.0/set_env.sh \
|
||||
VENV_ACTIVATE=/data/vllm-ascend-v0.18.0/.venv/bin/activate \
|
||||
MODEL_PATH=/data/models/Qwen2.5-1.5B-Instruct \
|
||||
DATA_DIR=/data/accuracy-gsm8k-gpqa \
|
||||
RESULTS_DIR=/data/v018-accuracy-results \
|
||||
ACCURACY_LIMIT=0 \
|
||||
bash scripts/run_accuracy_gsm8k_gpqa.sh
|
||||
```
|
||||
|
||||
For a plumbing smoke test, set `ACCURACY_LIMIT=2`; the runner then passes
|
||||
`--allow-limited` to the verifier. Limited mode relaxes only the formal row
|
||||
count and is not submission accuracy evidence.
|
||||
|
||||
The formal entry point uses the `qwen-chat-v1` protocol: both datasets are
|
||||
rendered with the model tokenizer's Qwen chat template, GSM8K requests a final
|
||||
line in the form `The answer is <number>.`, and GPQA requests one leading
|
||||
`A/B/C/D` choice. The default generation budgets are 512 tokens for GSM8K and
|
||||
8 tokens for GPQA.
|
||||
|
||||
After all six dataset/case runs, `verify_accuracy_matrix.py` checks dataset
|
||||
hashes, sample counts, claimed parameter settings, prompt protocol, sampling
|
||||
settings, seed, model, data source, and accuracy against the baseline. Its
|
||||
default formal mode additionally requires the fixed 1319/198 row counts, known
|
||||
dataset hashes, exact `qwen-chat-v1` metadata, temperature 0, seed 0, and
|
||||
512/8-token budgets. `MAX_ACCURACY_DROP=0` requires zero accuracy loss. It
|
||||
writes:
|
||||
|
||||
```text
|
||||
accuracy-summary.json
|
||||
accuracy-summary.csv
|
||||
accuracy-summary.md
|
||||
```
|
||||
|
||||
### Formal accuracy result
|
||||
|
||||
The corrected full run at
|
||||
`/data/reproduction-results/accuracy-fixed-bc33c33-formal-20260730` used
|
||||
temperature 0 and seed 0. No sample limit was applied.
|
||||
|
||||
| Dataset | Samples | Baseline | `group_8_num_1` | `group_8_num_2` | Accuracy change |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: |
|
||||
| GSM8K test | 1319 | 953/1319 (72.2517%) | 953/1319 (72.2517%) | 953/1319 (72.2517%) | 0.0000 |
|
||||
| GPQA Diamond | 198 | 47/198 (23.7374%) | 47/198 (23.7374%) | 47/198 (23.7374%) | 0.0000 |
|
||||
|
||||
`accuracy-summary.json` reports `verification_mode: formal` and
|
||||
`all_passed: true`. The generation used commit `bc33c33`; commit `e2cd297`
|
||||
tightened the parser and formal verifier, then revalidated the saved outputs.
|
||||
All 198 GPQA predictions per case are literal single letters, so the parser
|
||||
hardening does not change the measured score. In addition, the three GSM8K
|
||||
example JSONL files have the same SHA-256
|
||||
`79f223c71ae7265bd37d7fc79a582f3b896823ba8a0256b52cbe370df9267c20`,
|
||||
and the three GPQA example files have the same SHA-256
|
||||
`0f403ae99640dde7c6c27bd993da9acc62752b87a1f81a655ede98de058914aa`.
|
||||
Thus both claimed parameter cases reproduce the baseline output byte-for-byte.
|
||||
|
||||
All 1319 GSM8K outputs and all 198 GPQA outputs were parseable in every case.
|
||||
Only 12 GSM8K outputs per case reached the 512-token limit, compared with the
|
||||
obsolete raw-text protocol's near-total truncation; no GPQA output reached its
|
||||
8-token limit. The scores characterize this fixed 1.5B model and protocol and
|
||||
are not a general claim about model quality.
|
||||
|
||||
The downloaded archive
|
||||
`reproduction-results-20260730/accuracy-fixed-bc33c33-formal-20260730-e2cd297.tar.gz`
|
||||
is kept outside the Git worktree. Its SHA-256 is
|
||||
`b95b3c0ccc5b30ab166c058ba5e3fb7ca14e91197f35f514486b177512ca571f`.
|
||||
The archive includes `formal-run-manifest.json` with generation and
|
||||
verification commits, commands, runtime versions, formal settings, dataset
|
||||
hashes, complete model/tokenizer hashes, artifact hashes, and device state. It
|
||||
also includes the strict summary and the 78-test JUnit XML report.
|
||||
|
||||
## Unified Reproduction
|
||||
|
||||
```bash
|
||||
cd /data/vllm-ascend-v0.18.0
|
||||
MODEL_PATH=/data/models/Qwen2.5-1.5B-Instruct \
|
||||
DATA_DIR=/data/accuracy-gsm8k-gpqa \
|
||||
REPRO_RESULTS_DIR=/data/v018-reproduction-results \
|
||||
bash scripts/run_reproduce_all.sh
|
||||
```
|
||||
|
||||
The unified script runs performance before accuracy and writes `reproduction-manifest.json` with commands, package versions, source commit, model path, and artifact locations.
|
||||
|
||||
## Verified Server State
|
||||
|
||||
The July 21 performance environment mapped logical device 0 to physical NPU 0.
|
||||
On the migrated server used for the corrected July 30 accuracy run, logical
|
||||
device 0 mapped to physical NPU 6/chip 0. The device reported `Health: OK`, and
|
||||
`npu-smi info` reported no running NPU process after the matrix completed.
|
||||
|
||||
The environment contains an unrelated optional `msserviceprofiler` entry point
|
||||
whose module is absent. vLLM logs that plugin-load error and continues; Ascend
|
||||
platform activation, model execution, result generation, and shutdown all
|
||||
complete successfully.
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
# Test Evidence
|
||||
|
||||
## Script Tests
|
||||
|
||||
Command run on 2026-07-20:
|
||||
|
||||
```bash
|
||||
python -m unittest \
|
||||
code/tests/model_executor/offloader/test_acceptance_scripts.py \
|
||||
code/tests/model_executor/offloader/test_accuracy_repro.py
|
||||
```
|
||||
|
||||
Historical result on 2026-07-21:
|
||||
|
||||
```text
|
||||
Ran 35 tests in 0.341s
|
||||
OK
|
||||
```
|
||||
|
||||
After the accuracy protocol correction on 2026-07-30, the focused local
|
||||
regression results were:
|
||||
|
||||
```text
|
||||
test_accuracy_repro.py: 26/26 passed
|
||||
test_*scripts.py: 18/18 passed
|
||||
compileall: passed
|
||||
```
|
||||
|
||||
The complete remote offloader suite was then rerun in the CANN 9.0.0 Ascend
|
||||
environment:
|
||||
|
||||
```bash
|
||||
python -m pytest tests/model_executor/offloader -q
|
||||
```
|
||||
|
||||
```text
|
||||
78 passed in 11.77s
|
||||
```
|
||||
|
||||
## Remote Runtime
|
||||
|
||||
Historical July 21 performance runtime under
|
||||
`/data/vllm-ascend-v0.18.0/.venv`:
|
||||
|
||||
```text
|
||||
vllm 0.18.0+empty
|
||||
vllm-ascend 0.1.dev1+g72dc68973.d20260720
|
||||
torch 2.9.0+cpu
|
||||
torch-npu 2.9.0
|
||||
triton-ascend 3.2.0.dev20260322
|
||||
npu_available True
|
||||
npu_device_count 1
|
||||
```
|
||||
|
||||
The corrected July 30 accuracy runtime was:
|
||||
|
||||
```text
|
||||
Python 3.11.6
|
||||
vllm 0.18.0+empty
|
||||
vllm-ascend 0.1.dev1+g72dc68973.d20260729
|
||||
torch 2.9.0+cpu
|
||||
torch-npu 2.9.0.post2
|
||||
triton-ascend 3.2.1
|
||||
CANN 9.0.0
|
||||
```
|
||||
|
||||
The rebuilt `AddRmsNormBias` custom operator loaded and produced the expected values:
|
||||
|
||||
```text
|
||||
custom_enabled True
|
||||
result (2, 16) (2, 1) 1.0
|
||||
residual 2.0
|
||||
```
|
||||
|
||||
## Formal Performance Evidence
|
||||
|
||||
Command settings: 3 repeats, 1 warmup round, 3 measured rounds, 8 prompts,
|
||||
128 input tokens, and 64 output tokens. Nine raw runs were aggregated by median.
|
||||
|
||||
| Case | Output tok/s | Change | Saved weight storage | Available KV cache |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| `baseline_0_1` | 742.77 | 0.00% | 0.0000 GB | 27.27 GiB |
|
||||
| `group_8_num_1` | 678.85 | -8.61% | 0.2615 GB | 27.53 GiB |
|
||||
| `group_8_num_2` | 383.49 | -48.37% | 0.5230 GB | 27.79 GiB |
|
||||
|
||||
Remote artifacts: `/data/v018-performance-formal-20260721`.
|
||||
Local copy: `materials/results/v018-performance-formal-20260721`.
|
||||
|
||||
## Full Accuracy Evidence
|
||||
|
||||
The corrected formal matrix was rerun on 2026-07-30 after migrating the branch
|
||||
to the Ascend 910B2C server. It used protocol `qwen-chat-v1`, the model's Qwen
|
||||
chat template, `ACCURACY_LIMIT=0`, temperature 0, seed 0, and generation budgets
|
||||
of 512 tokens for GSM8K and 8 tokens for GPQA.
|
||||
|
||||
| Dataset | Rows | Baseline | `group_8_num_1` | `group_8_num_2` |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| GSM8K | 1319 | 953 (72.2517%) | 953 (72.2517%) | 953 (72.2517%) |
|
||||
| GPQA Diamond | 198 | 47 (23.7374%) | 47 (23.7374%) | 47 (23.7374%) |
|
||||
|
||||
The strict verifier returned `verification_mode: formal` and
|
||||
`all_passed: true` with `MAX_ACCURACY_DROP=0`. It now fixes the formal dataset
|
||||
hashes and row counts, exact protocol and sampling settings, seed 0, and
|
||||
cross-case model/data-source identity. The example JSONL hashes are identical
|
||||
across all three cases for each dataset, so the claimed parameter cases
|
||||
reproduce every baseline prediction byte-for-byte.
|
||||
|
||||
| Dataset | Parsed answers | At token limit | Example JSONL SHA-256 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| GSM8K | 1319/1319 | 12/1319 | `79f223c71ae7265bd37d7fc79a582f3b896823ba8a0256b52cbe370df9267c20` |
|
||||
| GPQA Diamond | 198/198 | 0/198 | `0f403ae99640dde7c6c27bd993da9acc62752b87a1f81a655ede98de058914aa` |
|
||||
|
||||
Remote artifacts:
|
||||
`/data/reproduction-results/accuracy-fixed-bc33c33-formal-20260730`.
|
||||
The immutable downloaded archive is
|
||||
`reproduction-results-20260730/accuracy-fixed-bc33c33-formal-20260730-e2cd297.tar.gz`
|
||||
outside the Git worktree, with SHA-256
|
||||
`b95b3c0ccc5b30ab166c058ba5e3fb7ca14e91197f35f514486b177512ca571f`.
|
||||
It includes `formal-run-manifest.json`, the complete model/tokenizer file
|
||||
hashes, the strict summaries, and `offloader-tests-e2cd297.xml`.
|
||||
|
||||
## Final Device Health
|
||||
|
||||
For the corrected accuracy run, logical runtime device 0 mapped to physical NPU
|
||||
6/chip 0. The server used CANN 9.0.0 and the corrected runtime listed above.
|
||||
After strict reverification:
|
||||
|
||||
```text
|
||||
NPU 6 910B2C
|
||||
Health: OK
|
||||
AICore: 0%
|
||||
No running processes found in NPU 6
|
||||
```
|
||||
|
||||
The optional `msserviceprofiler` entry point emits a non-fatal missing-module
|
||||
message. It does not affect Ascend activation, inference, summaries, or clean
|
||||
engine shutdown.
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
# vLLM Ascend v0.23.0rc1 部署测试与 v0.18.0 对比报告
|
||||
|
||||
- **测试日期:** 2026-07-30
|
||||
- **当前分支:** `feature/offload-group-num-v018-optimized`
|
||||
- **分支基准提交:** `0b12e7e`
|
||||
- **测试参数:** `offload_group_size`、`offload_num_in_group`
|
||||
|
||||
## 1. 结论摘要
|
||||
|
||||
截至测试日,官方最新发布为
|
||||
[vLLM Ascend v0.23.0rc1](https://github.com/vllm-project/vllm-ascend/releases/tag/v0.23.0rc1)。
|
||||
本次在 Ascend 910B2C 上安装官方二进制 wheel,并使用当前分支的复现脚本,
|
||||
完成基线、`group_8_num_1`、`group_8_num_2` 三种配置的正式性能矩阵和
|
||||
GSM8K、GPQA Diamond 全量精度矩阵。
|
||||
|
||||
主要结果如下:
|
||||
|
||||
- 官方最新版的两个参数均被实际执行。日志显示 `NPUPrefetchOffloader`
|
||||
分别初始化 3 个和 6 个模块,报告节省 0.2808 GB 和 0.5616 GB NPU
|
||||
权重空间。
|
||||
- v0.23.0rc1 的性能中位数分别为 580.92、559.37、381.37 tok/s。
|
||||
两个正向参数相对同版本基线下降 3.71% 和 34.35%。
|
||||
- GSM8K 三组均为 953/1319(72.2517%),GPQA Diamond 三组均为
|
||||
47/198(23.7374%),两个参数相对基线的精度下降均为 0。
|
||||
- 与 v0.18.0 相比,`group_8_num_1` 的同版本基线吞吐损耗从 8.61%
|
||||
缩小至 3.71%,改善 4.90 个百分点;`group_8_num_2` 从 48.37%
|
||||
缩小至 34.35%,改善 14.02 个百分点。
|
||||
- v0.23.0rc1 的三组逐题输出在各数据集内完全一致,GSM8K examples
|
||||
SHA256 均为 `79f223c71ae7...9267c20`,GPQA examples SHA256 均为
|
||||
`0f403ae99640...058914aa`。
|
||||
- 项目严格校验器和独立校验器均返回 `all_passed: true`;修复后的精度
|
||||
复现测试为 33/33 通过。
|
||||
|
||||
## 2. 对比对象
|
||||
|
||||
### 2.1 当前分支 v0.18.0
|
||||
|
||||
当前分支在竞赛目标版本 vLLM Ascend v0.18.0 上补齐了两个参数的 Ascend
|
||||
分组预取卸载实现。既有正式结果取自 `docs/v018-test-evidence.md`:
|
||||
|
||||
- vLLM Ascend 基线提交:
|
||||
`72dc68973bd7e6ceef9a122de316a7ee9c9a84aa`
|
||||
- CANN:8.5.1
|
||||
- 模型:Qwen2.5-1.5B-Instruct
|
||||
- 性能:固定 8 请求、128 输入 token、64 输出 token
|
||||
- 精度:GSM8K 1319 条、GPQA Diamond 198 条
|
||||
|
||||
### 2.2 官方最新版 v0.23.0rc1
|
||||
|
||||
本次测试使用官方发布物,不从当前仓库加载 `vllm_ascend` Python 包:
|
||||
|
||||
| 项目 | 版本或提交 |
|
||||
|---|---|
|
||||
| vLLM Ascend | `v0.23.0rc1` |
|
||||
| vLLM Ascend 源码提交 | `f4a08bddd0cc65a0bd8c3d377b158ae5ca7527db` |
|
||||
| 官方 wheel SHA256 | `2f02c7a31e6ed565084876aebb6cca0bc8e60d8988fdacb3d0f69f4caa8138c8` |
|
||||
| vLLM | `v0.23.0` |
|
||||
| vLLM 源码提交 | `0fc695fc6d1d82e9a5ac6835ac8e4e1c83703665` |
|
||||
| CANN / NNAL | 9.0.1 |
|
||||
| Python | 3.12.13 |
|
||||
| PyTorch / torch-npu | 2.10.0 / 2.10.0.post2 |
|
||||
| Triton Ascend | 3.2.1 |
|
||||
|
||||
上述依赖组合与
|
||||
[v0.23.0rc1 发布说明](https://github.com/vllm-project/vllm-ascend/releases/tag/v0.23.0rc1)
|
||||
和[官方安装文档](https://docs.vllm.ai/projects/ascend/en/latest/installation.html)
|
||||
一致。
|
||||
|
||||
## 3. 部署环境
|
||||
|
||||
| 项目 | 值 |
|
||||
|---|---|
|
||||
| 操作系统 | openEuler 24.03 (x86_64) |
|
||||
| NPU | Ascend 910B2C |
|
||||
| 测试设备 | 物理 NPU 6,运行时逻辑设备 0 |
|
||||
| 驱动 | 26.0.rc1 |
|
||||
| 模型路径 | `/data/models/Qwen2.5-1.5B-Instruct` |
|
||||
| 最新版虚拟环境 | `/data/vllm-ascend-v0.23.0rc1/.venv-cann901` |
|
||||
| 当前分支部署目录 | `/data/ccf-vllm-ascend-0b12e7e-lf` |
|
||||
| 服务器结果目录 | `/data/reproduction-results/v0230rc1-comparison-20260730` |
|
||||
|
||||
当前分支原始部署归档 SHA256 为
|
||||
`59c31c758c9530198461eda1408ec36e2b3941e217facb3cac85aa5aa06757f1`。
|
||||
正式测试结束后,仅对精度证据校验器及其回归测试做了本报告第 8 节所述修复;
|
||||
修复后文件 SHA256 已写入 `formal-test-manifest.json`。
|
||||
|
||||
## 4. 部署诊断
|
||||
|
||||
初次从仓库目录启动时,CANN `set_env.sh` 在 `PYTHONPATH` 末尾留下空路径。
|
||||
这会把当前工作目录加入模块搜索路径,使仓库中的局部 `vllm_ascend`
|
||||
命名空间遮蔽官方 wheel,触发缺少 `register` 的插件加载错误。
|
||||
|
||||
正式测试采用以下隔离措施:
|
||||
|
||||
1. 从 `env -i` 创建干净 shell。
|
||||
2. 依次加载 CANN 9.0.1、NNAL 和最新版虚拟环境。
|
||||
3. 删除 `PYTHONPATH` 末尾空元素。
|
||||
4. 在 `/data` 下调用复现脚本的绝对路径。
|
||||
5. 验证 `vllm_ascend.__file__` 指向虚拟环境中的官方 wheel。
|
||||
|
||||
官方 wheel 中的编译扩展、custom op vendor 目录和 NPU tensor 运算均验证通过。
|
||||
此前为诊断进行的无 custom kernel 源码安装及未完成的 178 kernel 源码构建均不计入
|
||||
正式结果。
|
||||
|
||||
## 5. 测试协议
|
||||
|
||||
### 5.1 性能
|
||||
|
||||
三种配置以 round-robin 顺序运行,每种配置重复 3 次,以中位数汇总:
|
||||
|
||||
| 参数 | 值 |
|
||||
|---|---:|
|
||||
| `REPEATS` | 3 |
|
||||
| `WARMUP_ROUNDS` | 1 |
|
||||
| `MEASURED_ROUNDS` | 3 |
|
||||
| `PROMPT_COUNT` | 8 |
|
||||
| `INPUT_TOKENS` | 128 |
|
||||
| `MAX_TOKENS` | 64 |
|
||||
| `MAX_MODEL_LEN` | 512 |
|
||||
| `GPU_MEMORY_UTILIZATION` | 0.5 |
|
||||
| `SEED` | 0 |
|
||||
| 正向 case 后端 | `prefetch` |
|
||||
|
||||
每次运行生成 1536 个输出 token。独立校验确认共有 9 条记录、每个 case
|
||||
恰好 3 条,且负载、随机种子、参数和后端均符合协议。
|
||||
|
||||
### 5.2 精度
|
||||
|
||||
| 数据集 | 样本数 | 数据 SHA256 | 最大输出 token |
|
||||
|---|---:|---|---:|
|
||||
| GSM8K test | 1319 | `32c548f08195...4a00595` | 512 |
|
||||
| GPQA Diamond | 198 | `0c16aaa98af0...6a0174` | 8 |
|
||||
|
||||
统一使用 `qwen-chat-v1`、chat template、温度 0、种子 0。GSM8K 解析
|
||||
最终数值答案,GPQA 解析开头单选答案。`ACCURACY_LIMIT=0`,因此是全量正式模式。
|
||||
|
||||
## 6. 性能结果
|
||||
|
||||
### 6.1 v0.23.0rc1
|
||||
|
||||
| 配置 | 三次吞吐范围 (tok/s) | 中位吞吐 (tok/s) | 相对基线 | P50 延迟 | 可用 KV cache | 插件报告节省 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| `baseline_0_1` | 477.07 - 594.95 | 580.92 | 0.00% | 0.8821 s | 27.27 GiB | 0 |
|
||||
| `group_8_num_1` | 548.71 - 569.25 | 559.37 | -3.71% | 0.9124 s | 27.53 GiB | 0.2808 GB |
|
||||
| `group_8_num_2` | 381.08 - 381.47 | 381.37 | -34.35% | 1.3423 s | 27.79 GiB | 0.5616 GB |
|
||||
|
||||
`group_8_num_1` 以 3.71% 吞吐代价增加约 0.26 GiB KV cache,是最新版中
|
||||
较温和的配置。`group_8_num_2` 进一步增加约 0.52 GiB KV cache,但吞吐代价
|
||||
上升至 34.35%。
|
||||
|
||||
### 6.2 与 v0.18.0 对比
|
||||
|
||||
| 配置 | v0.18.0 吞吐 | v0.23.0rc1 吞吐 | 绝对吞吐变化 | v0.18.0 相对基线 | v0.23.0rc1 相对基线 | 损耗变化 |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| `baseline_0_1` | 742.77 | 580.92 | -21.79% | 0.00% | 0.00% | - |
|
||||
| `group_8_num_1` | 678.85 | 559.37 | -17.60% | -8.61% | -3.71% | 改善 4.90 pp |
|
||||
| `group_8_num_2` | 383.49 | 381.37 | -0.55% | -48.37% | -34.35% | 改善 14.02 pp |
|
||||
|
||||
绝对吞吐跨版本同时受到 vLLM、vLLM Ascend、CANN、torch-npu 和调度器变化影响,
|
||||
不能全部归因于 offloader。更有可比性的指标是各版本内部相对同版本基线的损耗;
|
||||
按该指标,最新版两个配置的吞吐权衡均优于 v0.18.0 正式结果。
|
||||
|
||||
最新版插件报告的权重节省量从 v0.18.0 的 0.2615/0.5230 GB 增至
|
||||
0.2808/0.5616 GB,分别增加约 7.38%;两版报告的可用 KV cache
|
||||
均为 27.53/27.79 GiB。
|
||||
|
||||
## 7. 精度结果
|
||||
|
||||
| 数据集 | baseline | group 8 / num 1 | group 8 / num 2 | 两个参数最大下降 |
|
||||
|---|---:|---:|---:|---:|
|
||||
| GSM8K test | 953/1319 (72.2517%) | 953/1319 (72.2517%) | 953/1319 (72.2517%) | 0 |
|
||||
| GPQA Diamond | 47/198 (23.7374%) | 47/198 (23.7374%) | 47/198 (23.7374%) | 0 |
|
||||
|
||||
结果与 v0.18.0 正式精度完全相同。独立审计重新读取 6 个 examples JSONL,
|
||||
按物理 LF 记录逐条解析并重算 `correct`,确认结果 JSON 中的分数、总数、
|
||||
数据哈希、提示协议、采样参数、后端和输出路径全部一致。
|
||||
|
||||
本结果证明两个 offload 参数在固定模型和固定协议下未引入额外精度下降。
|
||||
23.7374% 是 Qwen2.5-1.5B-Instruct 在该 GPQA 提示协议下的基线表现,
|
||||
不代表其他模型或提示协议的通用水平。
|
||||
|
||||
## 8. 测试脚本修复
|
||||
|
||||
全量推理结束后,严格校验器最初报告
|
||||
`baseline_0_1/gpqa-examples.jsonl:5` 为截断 JSON。文件按真实换行逐条读取时
|
||||
实际完整,根因是 GPQA 原始字段中含有两个合法的 Unicode `U+2028`
|
||||
行分隔字符,而 Python `str.splitlines()` 会在该字符处切分字符串。
|
||||
|
||||
修复内容:
|
||||
|
||||
- `scripts/verify_accuracy_matrix.py` 改为仅按 JSONL 的 LF (`\n`) 分隔记录。
|
||||
- 新增包含 `U+2028` 的合法 JSON 字符串回归测试。
|
||||
- 本地和服务器完整精度复现测试均为 33/33 通过。
|
||||
- 不修改任何模型输出或分数文件,直接对原始 6 组结果重新校验通过。
|
||||
|
||||
## 9. 验证与证据
|
||||
|
||||
| 验证项 | 结果 |
|
||||
|---|---|
|
||||
| 官方 wheel NPU tensor 冒烟 | 通过 |
|
||||
| 3 case 性能冒烟 | 通过 |
|
||||
| 3 case × 2 dataset 精度冒烟 | 通过 |
|
||||
| 正式性能记录独立校验 | `all_passed: true` |
|
||||
| 项目正式精度校验 | `all_passed: true` |
|
||||
| 正式精度独立校验 | `all_passed: true` |
|
||||
| 精度脚本回归测试 | 33/33 |
|
||||
| 正式结果内部 SHA256 | 全部 `OK` |
|
||||
| 测试结束残留模型进程 | 0 |
|
||||
|
||||
本地结果目录:
|
||||
|
||||
```text
|
||||
D:\PDSL\Ascend\reproduction-results-v0230rc1-20260730
|
||||
```
|
||||
|
||||
本地压缩归档:
|
||||
|
||||
```text
|
||||
D:\PDSL\Ascend\v0230rc1-comparison-20260730-results.tar.gz
|
||||
SHA256: 5b8aeb9c1f8deb608e72ddfcdc4e0aaf36ae36b6b06ec67a3c3594376bc7549c
|
||||
```
|
||||
|
||||
关键证据:
|
||||
|
||||
- `formal-test-manifest.json`:版本、提交、wheel、环境和正式命令。
|
||||
- `performance/performance-runs.jsonl`:9 次原始性能记录。
|
||||
- `performance/performance-summary.json`:中位数汇总。
|
||||
- `performance-independent-verification.json`:性能独立校验。
|
||||
- `accuracy/*/*-examples.jsonl`:6 份逐题输出。
|
||||
- `accuracy/accuracy-summary.json`:正式精度汇总。
|
||||
- `accuracy-verification.json`:项目严格校验。
|
||||
- `accuracy-independent-verification.json`:精度独立重算。
|
||||
- `SHA256SUMS`:归档内正式证据校验和。
|
||||
|
||||
## 10. 已知告警与限制
|
||||
|
||||
- 最新版日志会提示空的 `VLLM_ASCEND_CUSTOM_OP_ROOT` 是未知 vLLM 环境变量;
|
||||
官方 wheel 的 custom op 路径和运行均已验证,告警不影响本次结果。
|
||||
- `pip check` 的剩余项来自可选 CANN profiler 组件版本约束及
|
||||
NumPy/OpenCV 约束,不影响 vLLM、torch-npu、NPU tensor 或正式矩阵。
|
||||
- vLLM 以源码 editable 方式安装,而原工作树指针已不存在,因此
|
||||
`pip freeze` 未能完整列出 vLLM;证据包同时保存 `pip list`、
|
||||
`runtime-versions.txt` 和精确源码提交。
|
||||
- 基线三次吞吐中有一次为 477.07 tok/s,低于另外两次。正式汇总按预先约定
|
||||
使用中位数,但单机三次重复仍不足以代表所有负载和硬件。
|
||||
- 官方 v0.23.0rc1 发布说明将 weight prefetch 列入未来计划废弃项;本报告只确认
|
||||
该精确版本上的行为,后续版本需重新验证参数可用性。
|
||||
|
||||
## 11. 复现命令
|
||||
|
||||
在已加载 CANN、NNAL 和最新版虚拟环境的干净 shell 中,从 `/data` 执行:
|
||||
|
||||
```bash
|
||||
export MODEL_PATH=/data/models/Qwen2.5-1.5B-Instruct
|
||||
export DATA_DIR=/data/accuracy-gsm8k-gpqa
|
||||
export VENV_ACTIVATE=/data/vllm-ascend-v0.23.0rc1/.venv-cann901/bin/activate
|
||||
export CANN_ENV=/data/Ascend/cann/set_env.sh
|
||||
export OFFLOAD_BACKEND=prefetch
|
||||
export PYTHONPATH=${PYTHONPATH%:}
|
||||
```
|
||||
|
||||
正式性能:
|
||||
|
||||
```bash
|
||||
RESULTS_DIR=/data/reproduction-results/v0230rc1-comparison-20260730/performance \
|
||||
REPEATS=3 WARMUP_ROUNDS=1 MEASURED_ROUNDS=3 \
|
||||
PROMPT_COUNT=8 INPUT_TOKENS=128 MAX_TOKENS=64 MAX_MODEL_LEN=512 \
|
||||
GPU_MEMORY_UTILIZATION=0.5 SEED=0 \
|
||||
bash /data/ccf-vllm-ascend-0b12e7e-lf/scripts/run_benchmark_all.sh
|
||||
```
|
||||
|
||||
正式精度:
|
||||
|
||||
```bash
|
||||
RESULTS_DIR=/data/reproduction-results/v0230rc1-comparison-20260730/accuracy \
|
||||
ACCURACY_LIMIT=0 MAX_MODEL_LEN=4096 \
|
||||
GSM8K_MAX_TOKENS=512 GPQA_MAX_TOKENS=8 MAX_ACCURACY_DROP=0 \
|
||||
GPU_MEMORY_UTILIZATION=0.5 SEED=0 \
|
||||
bash /data/ccf-vllm-ascend-0b12e7e-lf/scripts/run_accuracy_gsm8k_gpqa.sh
|
||||
```
|
||||
|
||||
## 12. 最终判断
|
||||
|
||||
官方 v0.23.0rc1 已完成部署、性能测试和全量精度测试。两个参赛参数在最新版中
|
||||
均实际生效并保持零精度下降;与 v0.18.0 相比,最新版在各自同版本基线下的
|
||||
相对吞吐损耗明显缩小,同时插件报告的卸载权重空间增加约 7.38%。
|
||||
|
||||
因此,最新版测试不是“尚未完成”或仅完成冒烟,而是已经形成包含原始输出、
|
||||
严格校验、独立重算、环境清单和 SHA256 的完整正式证据链。
|
||||
|
|
@ -0,0 +1,625 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Reproducible GSM8K and GPQA accuracy runner for Ascend offload comparisons."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter
|
||||
from collections.abc import Mapping
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
DEFAULT_DATASET_REVISION = "master"
|
||||
DATASET_SOURCES = {
|
||||
"gsm8k": {
|
||||
"name": "modelscope/gsm8k",
|
||||
"subset": "main",
|
||||
"revision": DEFAULT_DATASET_REVISION,
|
||||
"split": "test",
|
||||
},
|
||||
"gpqa": {
|
||||
"name": "modelscope/gpqa",
|
||||
"subset": "gpqa_diamond",
|
||||
"revision": DEFAULT_DATASET_REVISION,
|
||||
"split": None,
|
||||
},
|
||||
}
|
||||
PROMPT_PROTOCOL = "qwen-chat-v1"
|
||||
SYSTEM_MESSAGES = {
|
||||
"gsm8k": (
|
||||
"You are a precise math problem solver. "
|
||||
"Follow the requested answer format."
|
||||
),
|
||||
"gpqa": "You are a precise expert multiple-choice question solver.",
|
||||
}
|
||||
ANSWER_FORMATS = {
|
||||
"gsm8k": "final_numeric",
|
||||
"gpqa": "leading_choice",
|
||||
}
|
||||
NUMBER_RE = re.compile(r"[+-]?\d[\d,]*(?:\.\d+)?")
|
||||
GPQA_ANSWER_PATTERNS = (
|
||||
re.compile(
|
||||
r"^\s*(?:(?:the\s+)?answer\s*(?:is\s*|:\s*))"
|
||||
r"(?:\(([ABCD])\)|\[([ABCD])\]|([ABCD]))(?=$|[.,:;!?\n])",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
re.compile(
|
||||
r"^\s*(?:\(([ABCD])\)|\[([ABCD])\])(?=$|[.,:;!?\n])",
|
||||
re.IGNORECASE,
|
||||
),
|
||||
re.compile(r"^\s*([ABCD])(?=$|[.,:;!?\n])", re.IGNORECASE),
|
||||
)
|
||||
|
||||
|
||||
def extract_gsm8k_answer(text):
|
||||
"""Return the final signed numeric answer, preferring GSM8K's #### marker."""
|
||||
answer_text = str(text).rsplit("####", 1)[-1]
|
||||
matches = NUMBER_RE.findall(answer_text)
|
||||
return matches[-1].replace(",", "") if matches else None
|
||||
|
||||
|
||||
def normalize_numeric_answer(value):
|
||||
answer = extract_gsm8k_answer(value)
|
||||
if answer is None:
|
||||
return None
|
||||
try:
|
||||
decimal = Decimal(answer)
|
||||
except InvalidOperation:
|
||||
return None
|
||||
normalized = format(decimal.normalize(), "f")
|
||||
return normalized.rstrip("0").rstrip(".") if "." in normalized else normalized
|
||||
|
||||
|
||||
def score_gsm8k(gold, prediction):
|
||||
gold_answer = normalize_numeric_answer(gold)
|
||||
prediction_answer = normalize_numeric_answer(prediction)
|
||||
return gold_answer is not None and gold_answer == prediction_answer
|
||||
|
||||
|
||||
def normalize_gpqa_row(row):
|
||||
"""Normalize GPQA's title-case and snake-case data schemas."""
|
||||
normalized_keys = {
|
||||
key.lower().replace(" ", "_"): value for key, value in row.items()
|
||||
}
|
||||
question = normalized_keys.get("question")
|
||||
correct = normalized_keys.get("correct_answer")
|
||||
incorrect = [normalized_keys.get(f"incorrect_answer_{index}") for index in range(1, 4)]
|
||||
if not isinstance(question, str) or not isinstance(correct, str) or not all(
|
||||
isinstance(answer, str) for answer in incorrect
|
||||
):
|
||||
raise ValueError("GPQA row is missing a question or all four labeled answers")
|
||||
return {
|
||||
"question": question,
|
||||
"correct_answer": correct,
|
||||
"incorrect_answers": incorrect,
|
||||
}
|
||||
|
||||
|
||||
def build_gpqa_choices(row, seed):
|
||||
"""Build a question-stable A-D mapping without mutating global random state."""
|
||||
normalized = normalize_gpqa_row(row)
|
||||
choices = [(normalized["correct_answer"], True)] + [
|
||||
(answer, False) for answer in normalized["incorrect_answers"]
|
||||
]
|
||||
digest = hashlib.sha256(f"{seed}{normalized['question']}".encode("utf-8")).digest()
|
||||
random.Random(int.from_bytes(digest, "big")).shuffle(choices)
|
||||
correct_index = next(index for index, (_, is_correct) in enumerate(choices) if is_correct)
|
||||
return {
|
||||
"question": normalized["question"],
|
||||
"choices": [answer for answer, _ in choices],
|
||||
"correct_index": correct_index,
|
||||
}
|
||||
|
||||
|
||||
def parse_gpqa_answer(text):
|
||||
prediction = str(text).strip()
|
||||
for pattern in GPQA_ANSWER_PATTERNS:
|
||||
match = pattern.match(prediction)
|
||||
if match:
|
||||
return next(group for group in match.groups() if group).upper()
|
||||
return None
|
||||
|
||||
|
||||
def chat_messages_for_prompt(dataset, prompt):
|
||||
if dataset not in SYSTEM_MESSAGES:
|
||||
raise ValueError(f"unsupported prompt dataset: {dataset}")
|
||||
return [
|
||||
{"role": "system", "content": SYSTEM_MESSAGES[dataset]},
|
||||
{"role": "user", "content": str(prompt)},
|
||||
]
|
||||
|
||||
|
||||
def prompt_protocol_metadata(dataset):
|
||||
if dataset not in ANSWER_FORMATS:
|
||||
raise ValueError(f"unsupported prompt dataset: {dataset}")
|
||||
return {
|
||||
"name": PROMPT_PROTOCOL,
|
||||
"chat_template": True,
|
||||
"answer_format": ANSWER_FORMATS[dataset],
|
||||
}
|
||||
|
||||
|
||||
def dataset_sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with Path(path).open("rb") as file:
|
||||
for chunk in iter(lambda: file.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def jsonl_path(data_dir, dataset):
|
||||
return Path(data_dir) / f"{dataset}.jsonl"
|
||||
|
||||
|
||||
def manifest_path(data_dir, dataset):
|
||||
return Path(data_dir) / f"{dataset}.manifest.json"
|
||||
|
||||
|
||||
def _rows_from_modelscope(dataset):
|
||||
return [dict(row) for row in dataset]
|
||||
|
||||
|
||||
def _validated_gpqa_rows(dataset):
|
||||
rows = _rows_from_modelscope(dataset)
|
||||
if not rows:
|
||||
raise ValueError("GPQA split is empty")
|
||||
for row in rows:
|
||||
normalize_gpqa_row(row)
|
||||
return rows
|
||||
|
||||
|
||||
def _load_modelscope_rows(dataset_name, revision=DEFAULT_DATASET_REVISION):
|
||||
"""Load ModelScope only when local materialized data is unavailable."""
|
||||
from modelscope.msdatasets import MsDataset
|
||||
|
||||
source = dict(DATASET_SOURCES[dataset_name])
|
||||
source["revision"] = revision
|
||||
if dataset_name == "gsm8k":
|
||||
dataset = MsDataset.load(
|
||||
source["name"],
|
||||
subset_name=source["subset"],
|
||||
version=source["revision"],
|
||||
split=source["split"],
|
||||
trust_remote_code=True,
|
||||
)
|
||||
return _rows_from_modelscope(dataset), source
|
||||
|
||||
# GPQA publishes labeled configurations under gpqa_diamond. Probe split metadata
|
||||
# through the dataset object, then accept only the first split with labeled rows.
|
||||
metadata = MsDataset.load(
|
||||
source["name"],
|
||||
subset_name=source["subset"],
|
||||
version=source["revision"],
|
||||
trust_remote_code=True,
|
||||
)
|
||||
errors = []
|
||||
if isinstance(metadata, Mapping):
|
||||
for split, split_dataset in metadata.items():
|
||||
try:
|
||||
rows = _validated_gpqa_rows(split_dataset)
|
||||
source["split"] = str(split)
|
||||
return rows, source
|
||||
except (TypeError, ValueError) as error:
|
||||
errors.append(f"{split}: {error}")
|
||||
raise RuntimeError(
|
||||
"Unable to discover a labeled GPQA split in mapping: " + "; ".join(errors)
|
||||
)
|
||||
|
||||
split_names = getattr(getattr(metadata, "info", None), "splits", {}) or {}
|
||||
candidates = list(split_names) or ["test", "train", "validation"]
|
||||
for split in candidates:
|
||||
try:
|
||||
rows = _validated_gpqa_rows(
|
||||
MsDataset.load(
|
||||
source["name"],
|
||||
subset_name=source["subset"],
|
||||
version=source["revision"],
|
||||
split=split,
|
||||
trust_remote_code=True,
|
||||
)
|
||||
)
|
||||
source["split"] = split
|
||||
return rows, source
|
||||
except Exception as error: # Keep a complete diagnostic if no labeled split exists.
|
||||
errors.append(f"{split}: {error}")
|
||||
raise RuntimeError("Unable to discover a labeled GPQA split: " + "; ".join(errors))
|
||||
|
||||
|
||||
def _validated_jsonl_row_count(path, dataset):
|
||||
row_count = 0
|
||||
with Path(path).open(encoding="utf-8") as file:
|
||||
for line_number, line in enumerate(file, 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
row = json.loads(line)
|
||||
except json.JSONDecodeError as error:
|
||||
raise RuntimeError(f"Invalid JSONL at {path}:{line_number}") from error
|
||||
if not isinstance(row, dict):
|
||||
raise RuntimeError(f"Invalid {dataset} row fields at {path}:{line_number}")
|
||||
if dataset == "gsm8k":
|
||||
if not isinstance(row.get("question"), str) or not isinstance(
|
||||
row.get("answer"), str
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"Invalid GSM8K row fields at {path}:{line_number}"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
normalize_gpqa_row(row)
|
||||
except ValueError as error:
|
||||
raise RuntimeError(
|
||||
f"Invalid GPQA row fields at {path}:{line_number}"
|
||||
) from error
|
||||
row_count += 1
|
||||
if row_count == 0:
|
||||
raise RuntimeError(f"No examples found in {path}")
|
||||
return row_count
|
||||
|
||||
|
||||
def _write_dataset_manifest(metadata_path, dataset, path, source, data_hash, row_count):
|
||||
manifest = {
|
||||
"dataset": dataset,
|
||||
"dataset_path": str(path.resolve()),
|
||||
"dataset_sha256": data_hash,
|
||||
"dataset_source": source,
|
||||
"row_count": row_count,
|
||||
}
|
||||
metadata_path.write_text(
|
||||
json.dumps(manifest, ensure_ascii=False, sort_keys=True, indent=2) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def materialize_dataset(data_dir, dataset, revision=DEFAULT_DATASET_REVISION):
|
||||
path = jsonl_path(data_dir, dataset)
|
||||
metadata_path = manifest_path(data_dir, dataset)
|
||||
if path.exists():
|
||||
if not path.is_file() or path.stat().st_size == 0:
|
||||
raise RuntimeError(f"Local dataset is invalid: {path}")
|
||||
if not metadata_path.is_file():
|
||||
row_count = _validated_jsonl_row_count(path, dataset)
|
||||
source = {
|
||||
"name": "local",
|
||||
"subset": "local",
|
||||
"revision": "local",
|
||||
"split": "local",
|
||||
}
|
||||
data_hash = dataset_sha256(path)
|
||||
_write_dataset_manifest(
|
||||
metadata_path, dataset, path, source, data_hash, row_count
|
||||
)
|
||||
return path, source, data_hash
|
||||
try:
|
||||
manifest = json.loads(metadata_path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as error:
|
||||
raise RuntimeError(f"Invalid dataset manifest: {metadata_path}") from error
|
||||
required_keys = {
|
||||
"dataset",
|
||||
"dataset_path",
|
||||
"dataset_source",
|
||||
"dataset_sha256",
|
||||
"row_count",
|
||||
}
|
||||
if not required_keys.issubset(manifest) or manifest["dataset"] != dataset:
|
||||
raise RuntimeError(f"Dataset manifest does not describe {dataset}: {metadata_path}")
|
||||
if manifest["dataset_path"] != str(path.resolve()):
|
||||
raise RuntimeError(f"Dataset path does not match manifest: {path}")
|
||||
claimed_row_count = manifest["row_count"]
|
||||
if type(claimed_row_count) is not int or claimed_row_count <= 0:
|
||||
raise RuntimeError(f"Dataset manifest row_count must be a positive integer: {metadata_path}")
|
||||
source = manifest["dataset_source"]
|
||||
if not isinstance(source, dict) or not {
|
||||
"name",
|
||||
"subset",
|
||||
"revision",
|
||||
"split",
|
||||
}.issubset(source):
|
||||
raise RuntimeError(f"Dataset manifest source metadata is incomplete: {metadata_path}")
|
||||
actual_row_count = _validated_jsonl_row_count(path, dataset)
|
||||
if claimed_row_count != actual_row_count:
|
||||
raise RuntimeError(
|
||||
f"Dataset row_count does not match manifest: {claimed_row_count} != {actual_row_count}"
|
||||
)
|
||||
actual_hash = dataset_sha256(path)
|
||||
if manifest["dataset_sha256"] != actual_hash:
|
||||
raise RuntimeError(f"Dataset hash does not match manifest: {path}")
|
||||
return path, source, actual_hash
|
||||
|
||||
rows, source = _load_modelscope_rows(dataset, revision=revision)
|
||||
if not rows:
|
||||
raise RuntimeError(f"ModelScope returned no rows for {dataset}")
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", encoding="utf-8", newline="\n") as file:
|
||||
for row in rows:
|
||||
file.write(json.dumps(row, ensure_ascii=False, sort_keys=True, separators=(",", ":")))
|
||||
file.write("\n")
|
||||
row_count = _validated_jsonl_row_count(path, dataset)
|
||||
data_hash = dataset_sha256(path)
|
||||
_write_dataset_manifest(metadata_path, dataset, path, source, data_hash, row_count)
|
||||
return path, source, data_hash
|
||||
|
||||
|
||||
def load_local_rows(path, limit):
|
||||
rows = []
|
||||
with Path(path).open(encoding="utf-8") as file:
|
||||
for line_number, line in enumerate(file, 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
rows.append(json.loads(line))
|
||||
except json.JSONDecodeError as error:
|
||||
raise RuntimeError(f"Invalid JSONL at {path}:{line_number}") from error
|
||||
if limit and len(rows) >= limit:
|
||||
break
|
||||
if not rows:
|
||||
raise RuntimeError(f"No examples found in {path}")
|
||||
return rows
|
||||
|
||||
|
||||
def prompt_for_example(dataset, row, seed):
|
||||
if dataset == "gsm8k":
|
||||
instruction = (
|
||||
"Solve the following math word problem step by step. "
|
||||
"End your response with a separate final line exactly in this format: "
|
||||
"The answer is <number>. "
|
||||
"Do not write anything after the final answer line."
|
||||
)
|
||||
return (
|
||||
f"{instruction}\n\nQuestion:\n{row['question']}",
|
||||
str(row["answer"]),
|
||||
)
|
||||
choices = build_gpqa_choices(row, seed)
|
||||
options = "\n".join(
|
||||
f"{chr(65 + index)}. {choice}" for index, choice in enumerate(choices["choices"])
|
||||
)
|
||||
instruction = (
|
||||
"Select the single best answer to the following multiple-choice question. "
|
||||
"Return only one capital letter: A, B, C, or D. "
|
||||
"Do not include an explanation."
|
||||
)
|
||||
return (
|
||||
f"{instruction}\n\nQuestion:\n{choices['question']}"
|
||||
f"\n\nChoices:\n{options}\n\nAnswer:",
|
||||
chr(65 + choices["correct_index"]),
|
||||
)
|
||||
|
||||
|
||||
def render_chat_prompts(tokenizer, dataset, prompts):
|
||||
rendered_prompts = []
|
||||
for prompt in prompts:
|
||||
try:
|
||||
rendered = tokenizer.apply_chat_template(
|
||||
chat_messages_for_prompt(dataset, prompt),
|
||||
tokenize=False,
|
||||
add_generation_prompt=True,
|
||||
)
|
||||
except Exception as error:
|
||||
raise RuntimeError(
|
||||
f"unable to apply chat template for {dataset}"
|
||||
) from error
|
||||
if not isinstance(rendered, str) or not rendered.strip():
|
||||
raise RuntimeError(f"unable to apply chat template for {dataset}")
|
||||
rendered_prompts.append(rendered)
|
||||
return rendered_prompts
|
||||
|
||||
|
||||
def build_llm_kwargs(args):
|
||||
kwargs = {
|
||||
"model": args.model,
|
||||
"enforce_eager": True,
|
||||
"max_model_len": args.max_model_len,
|
||||
"gpu_memory_utilization": args.gpu_memory_utilization,
|
||||
"offload_group_size": args.offload_group_size,
|
||||
"offload_num_in_group": args.offload_num_in_group,
|
||||
}
|
||||
if getattr(args, "offload_backend", None) is not None:
|
||||
kwargs["offload_backend"] = args.offload_backend
|
||||
return kwargs
|
||||
|
||||
|
||||
def generate_prompts(args, dataset, prompts):
|
||||
"""Generate eagerly, importing the vLLM runtime only for an actual run."""
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
llm = LLM(**build_llm_kwargs(args))
|
||||
rendered_prompts = render_chat_prompts(llm.get_tokenizer(), dataset, prompts)
|
||||
sampling = SamplingParams(temperature=0, max_tokens=args.max_tokens)
|
||||
outputs = llm.generate(rendered_prompts, sampling)
|
||||
records = []
|
||||
for output in outputs:
|
||||
candidates = getattr(output, "outputs", None) or []
|
||||
if not candidates:
|
||||
records.append(
|
||||
{
|
||||
"text": "",
|
||||
"output_token_count": 0,
|
||||
"finish_reason": "missing",
|
||||
}
|
||||
)
|
||||
continue
|
||||
candidate = candidates[0]
|
||||
token_ids = getattr(candidate, "token_ids", None) or []
|
||||
records.append(
|
||||
{
|
||||
"text": str(getattr(candidate, "text", "")),
|
||||
"output_token_count": len(token_ids),
|
||||
"finish_reason": getattr(candidate, "finish_reason", None)
|
||||
or "unknown",
|
||||
}
|
||||
)
|
||||
return records
|
||||
|
||||
|
||||
def summarize_generation(records, max_tokens):
|
||||
finish_reasons = Counter(record["finish_reason"] for record in records)
|
||||
return {
|
||||
"at_token_limit": sum(
|
||||
record["output_token_count"] >= max_tokens for record in records
|
||||
),
|
||||
"finish_reasons": dict(finish_reasons),
|
||||
"output_tokens": sum(record["output_token_count"] for record in records),
|
||||
}
|
||||
|
||||
|
||||
def build_result_metadata(
|
||||
args,
|
||||
dataset_source,
|
||||
dataset_sha256,
|
||||
correct,
|
||||
total,
|
||||
elapsed_seconds,
|
||||
output_path,
|
||||
generation_summary,
|
||||
):
|
||||
return {
|
||||
"model": args.model,
|
||||
"dataset": args.dataset,
|
||||
"dataset_source": dataset_source,
|
||||
"dataset_sha256": dataset_sha256,
|
||||
"seed": args.seed,
|
||||
"prompt_protocol": prompt_protocol_metadata(args.dataset),
|
||||
"sampling_settings": {
|
||||
"temperature": args.temperature,
|
||||
"max_tokens": args.max_tokens,
|
||||
"enforce_eager": True,
|
||||
},
|
||||
"engine_settings": {
|
||||
"gpu_memory_utilization": args.gpu_memory_utilization,
|
||||
},
|
||||
"offload_settings": {
|
||||
"backend": getattr(args, "offload_backend", None),
|
||||
"group_size": args.offload_group_size,
|
||||
"num_in_group": args.offload_num_in_group,
|
||||
},
|
||||
"correct": correct,
|
||||
"total": total,
|
||||
"accuracy": correct / total if total else 0.0,
|
||||
"elapsed_seconds": elapsed_seconds,
|
||||
"generation_summary": generation_summary,
|
||||
"output_path": str(output_path),
|
||||
}
|
||||
|
||||
|
||||
def run_dataset(args, dataset):
|
||||
args.dataset = dataset
|
||||
data_path, source, data_hash = materialize_dataset(
|
||||
args.data_dir, dataset, revision=args.dataset_revision
|
||||
)
|
||||
rows = load_local_rows(data_path, args.limit)
|
||||
prompts_and_gold = [prompt_for_example(dataset, row, args.seed) for row in rows]
|
||||
prompts = [prompt for prompt, _ in prompts_and_gold]
|
||||
started = time.monotonic()
|
||||
generation_records = generate_prompts(args, dataset, prompts)
|
||||
elapsed_seconds = time.monotonic() - started
|
||||
if len(generation_records) != len(rows):
|
||||
raise RuntimeError("Generation returned a different number of predictions")
|
||||
|
||||
output_dir = Path(args.output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
examples_path = output_dir / f"{dataset}-examples.jsonl"
|
||||
correct = 0
|
||||
parsed_answers = 0
|
||||
with examples_path.open("w", encoding="utf-8", newline="\n") as file:
|
||||
for row, (_, gold), record in zip(
|
||||
rows, prompts_and_gold, generation_records
|
||||
):
|
||||
prediction = record["text"]
|
||||
parsed_answer = (
|
||||
normalize_numeric_answer(prediction)
|
||||
if dataset == "gsm8k"
|
||||
else parse_gpqa_answer(prediction)
|
||||
)
|
||||
parsed_answers += parsed_answer is not None
|
||||
is_correct = (
|
||||
score_gsm8k(gold, prediction)
|
||||
if dataset == "gsm8k"
|
||||
else parsed_answer == gold
|
||||
)
|
||||
correct += is_correct
|
||||
file.write(
|
||||
json.dumps(
|
||||
{
|
||||
"row": row,
|
||||
"gold": gold,
|
||||
"prediction": prediction,
|
||||
"parsed_answer": parsed_answer,
|
||||
"output_token_count": record["output_token_count"],
|
||||
"finish_reason": record["finish_reason"],
|
||||
"correct": is_correct,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
sort_keys=True,
|
||||
)
|
||||
)
|
||||
file.write("\n")
|
||||
|
||||
generation_summary = summarize_generation(generation_records, args.max_tokens)
|
||||
generation_summary["parsed_answers"] = parsed_answers
|
||||
result = build_result_metadata(
|
||||
args,
|
||||
source,
|
||||
data_hash,
|
||||
correct,
|
||||
len(rows),
|
||||
elapsed_seconds,
|
||||
examples_path,
|
||||
generation_summary,
|
||||
)
|
||||
result_path = output_dir / f"{dataset}-result.json"
|
||||
result_path.write_text(json.dumps(result, ensure_ascii=False, sort_keys=True, indent=2) + "\n", encoding="utf-8")
|
||||
return result
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--model", required=True)
|
||||
parser.add_argument("--dataset", choices=("gsm8k", "gpqa", "both"), default="both")
|
||||
parser.add_argument("--data-dir", type=Path, default=Path("/data/accuracy-data"))
|
||||
parser.add_argument("--output-dir", type=Path, default=Path("/data/accuracy-results"))
|
||||
parser.add_argument("--limit", type=int, default=0, help="0 means the full dataset")
|
||||
parser.add_argument("--seed", default="0")
|
||||
parser.add_argument("--dataset-revision", default=DEFAULT_DATASET_REVISION)
|
||||
parser.add_argument("--max-tokens", type=int, default=512)
|
||||
parser.add_argument("--max-model-len", type=int, default=4096)
|
||||
parser.add_argument("--gpu-memory-utilization", type=float, default=0.5)
|
||||
parser.add_argument("--offload-group-size", type=int, default=0)
|
||||
parser.add_argument("--offload-num-in-group", type=int, default=1)
|
||||
parser.add_argument("--offload-backend", choices=("auto", "prefetch", "uva"))
|
||||
args = parser.parse_args(argv)
|
||||
args.temperature = 0
|
||||
if args.limit < 0:
|
||||
parser.error("--limit must be non-negative; 0 means the full dataset")
|
||||
if args.max_tokens <= 0 or args.max_model_len <= 0:
|
||||
parser.error("--max-tokens and --max-model-len must be positive")
|
||||
if not 0 < args.gpu_memory_utilization <= 1:
|
||||
parser.error("--gpu-memory-utilization must be in (0, 1]")
|
||||
if args.offload_group_size < 0:
|
||||
parser.error("--offload-group-size must be non-negative")
|
||||
if args.offload_num_in_group <= 0:
|
||||
parser.error("--offload-num-in-group must be positive")
|
||||
if args.offload_group_size == 0 and args.offload_num_in_group != 1:
|
||||
parser.error("group size 0 requires --offload-num-in-group 1")
|
||||
if args.offload_group_size and args.offload_num_in_group > args.offload_group_size:
|
||||
parser.error("--offload-num-in-group cannot exceed --offload-group-size")
|
||||
return args
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = parse_args(argv)
|
||||
datasets = ("gsm8k", "gpqa") if args.dataset == "both" else (args.dataset,)
|
||||
results = [run_dataset(args, dataset) for dataset in datasets]
|
||||
print(json.dumps(results, ensure_ascii=False, sort_keys=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except Exception as error:
|
||||
print(f"accuracy reproduction failed: {error}", file=sys.stderr)
|
||||
raise
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,530 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Validate and summarize the v0.18 offload parameter benchmark matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import re
|
||||
import statistics
|
||||
from collections import Counter, defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
|
||||
CASE_ORDER = (
|
||||
"baseline_0_1",
|
||||
"group_28_num_1",
|
||||
"group_14_num_1",
|
||||
"group_28_num_2",
|
||||
"group_8_num_1",
|
||||
"group_7_num_1",
|
||||
"group_14_num_2",
|
||||
"group_8_num_2",
|
||||
"group_4_num_1",
|
||||
"group_2_num_1",
|
||||
"group_4_num_2",
|
||||
)
|
||||
EXPECTED_MAIN_COUNTS = {
|
||||
label: 5 if label in CASE_ORDER[:5] else 3 for label in CASE_ORDER
|
||||
}
|
||||
CONFIRMATION_ORDER = (
|
||||
"baseline_0_1",
|
||||
"group_28_num_1",
|
||||
"group_14_num_1",
|
||||
)
|
||||
EXPECTED_CONFIRMATION_COUNTS = {
|
||||
label: 3 for label in CONFIRMATION_ORDER
|
||||
}
|
||||
OFFLOADER_RE = re.compile(
|
||||
r"Ascend weight offloader ready: "
|
||||
r"selected_layers=(?P<layers>\([^)]*\)), "
|
||||
r"wrapped=(?P<wrapped>\d+), "
|
||||
r"saved=(?P<saved>\d+(?:\.\d+)?) GB, "
|
||||
r"static_buffer=(?P<buffer>\d+(?:\.\d+)?) GB"
|
||||
)
|
||||
KV_CACHE_RE = re.compile(r"Available KV cache memory: (?P<gib>\d+(?:\.\d+)?) GiB")
|
||||
|
||||
|
||||
def load_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
records: list[dict[str, Any]] = []
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
for line_number, line in enumerate(handle, 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
record = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(
|
||||
f"invalid JSON on {path}:{line_number}: {exc}"
|
||||
) from exc
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError(f"record on {path}:{line_number} is not an object")
|
||||
records.append(record)
|
||||
if not records:
|
||||
raise ValueError(f"no benchmark records found in {path}")
|
||||
return records
|
||||
|
||||
|
||||
def percent_change(value: float, baseline: float) -> float:
|
||||
if baseline == 0:
|
||||
raise ValueError("baseline must be non-zero")
|
||||
return (value - baseline) / baseline * 100.0
|
||||
|
||||
|
||||
def sample_stdev(values: list[float]) -> float:
|
||||
return statistics.stdev(values) if len(values) > 1 else 0.0
|
||||
|
||||
|
||||
def pooled_round_throughputs(records: Iterable[dict[str, Any]]) -> list[float]:
|
||||
rates: list[float] = []
|
||||
for record in records:
|
||||
measured_rounds = int(record["measured_rounds"])
|
||||
latencies = [float(value) for value in record["round_latencies_seconds"]]
|
||||
if measured_rounds != len(latencies):
|
||||
raise ValueError(
|
||||
f"{record.get('label')}: measured_rounds does not match latencies"
|
||||
)
|
||||
output_per_round = float(record["output_tokens"]) / measured_rounds
|
||||
rates.extend(output_per_round / latency for latency in latencies)
|
||||
return rates
|
||||
|
||||
|
||||
def label_from_log(path: Path) -> str:
|
||||
marker = "_repeat_"
|
||||
if marker not in path.stem:
|
||||
raise ValueError(f"unexpected benchmark log name: {path.name}")
|
||||
return path.stem.rsplit(marker, 1)[0]
|
||||
|
||||
|
||||
def parse_log_metadata(
|
||||
paths: Iterable[Path],
|
||||
) -> tuple[dict[str, dict[str, Any]], list[str]]:
|
||||
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
errors: list[str] = []
|
||||
for path in paths:
|
||||
text = path.read_text(encoding="utf-8", errors="replace")
|
||||
offloader_matches = list(OFFLOADER_RE.finditer(text))
|
||||
kv_matches = list(KV_CACHE_RE.finditer(text))
|
||||
if len(offloader_matches) != 1 or len(kv_matches) != 1:
|
||||
errors.append(
|
||||
f"{path.name}: expected one offloader and one KV-cache record"
|
||||
)
|
||||
continue
|
||||
match = offloader_matches[0]
|
||||
grouped[label_from_log(path)].append(
|
||||
{
|
||||
"selected_layers": match.group("layers"),
|
||||
"offloaded_layer_count": int(match.group("wrapped")),
|
||||
"weight_storage_saved_gb": float(match.group("saved")),
|
||||
"static_buffer_gb": float(match.group("buffer")),
|
||||
"kv_cache_available_gib": float(kv_matches[0].group("gib")),
|
||||
}
|
||||
)
|
||||
|
||||
metadata: dict[str, dict[str, Any]] = {}
|
||||
for label, rows in grouped.items():
|
||||
layer_sets = {row["selected_layers"] for row in rows}
|
||||
wrapped_counts = {row["offloaded_layer_count"] for row in rows}
|
||||
if len(layer_sets) != 1 or len(wrapped_counts) != 1:
|
||||
errors.append(f"{label}: inconsistent selected layers across logs")
|
||||
continue
|
||||
metadata[label] = {
|
||||
"selected_layers": next(iter(layer_sets)),
|
||||
"offloaded_layer_count": next(iter(wrapped_counts)),
|
||||
"weight_storage_saved_gb": statistics.median(
|
||||
row["weight_storage_saved_gb"] for row in rows
|
||||
),
|
||||
"static_buffer_gb": statistics.median(
|
||||
row["static_buffer_gb"] for row in rows
|
||||
),
|
||||
"kv_cache_available_median_gib": statistics.median(
|
||||
row["kv_cache_available_gib"] for row in rows
|
||||
),
|
||||
}
|
||||
return metadata, errors
|
||||
|
||||
|
||||
def summarize_case(
|
||||
label: str,
|
||||
records: list[dict[str, Any]],
|
||||
metadata: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
process_rates = [
|
||||
float(record["output_tokens_per_second"]) for record in records
|
||||
]
|
||||
round_rates = pooled_round_throughputs(records)
|
||||
first = records[0]
|
||||
return {
|
||||
"label": label,
|
||||
"offload_group_size": int(first["offload_group_size"]),
|
||||
"offload_num_in_group": int(first["offload_num_in_group"]),
|
||||
"repeat_count": len(records),
|
||||
"throughput_runs_tok_s": process_rates,
|
||||
"throughput_median_tok_s": statistics.median(process_rates),
|
||||
"throughput_mean_tok_s": statistics.mean(process_rates),
|
||||
"throughput_stdev_tok_s": sample_stdev(process_rates),
|
||||
"throughput_min_tok_s": min(process_rates),
|
||||
"throughput_max_tok_s": max(process_rates),
|
||||
"pooled_round_count": len(round_rates),
|
||||
"pooled_round_median_tok_s": statistics.median(round_rates),
|
||||
"pooled_round_mean_tok_s": statistics.mean(round_rates),
|
||||
"pooled_round_stdev_tok_s": sample_stdev(round_rates),
|
||||
"latency_p50_median_s": statistics.median(
|
||||
float(record["latency_p50_seconds"]) for record in records
|
||||
),
|
||||
"load_time_median_s": statistics.median(
|
||||
float(record["load_seconds"]) for record in records
|
||||
),
|
||||
**metadata,
|
||||
}
|
||||
|
||||
|
||||
def mark_pareto(rows: list[dict[str, Any]], baseline_label: str) -> None:
|
||||
candidates = [row for row in rows if row["label"] != baseline_label]
|
||||
for row in rows:
|
||||
if row["label"] == baseline_label:
|
||||
row["pareto_optimal"] = None
|
||||
row["dominated_by"] = []
|
||||
continue
|
||||
dominated_by = []
|
||||
for other in candidates:
|
||||
if other is row:
|
||||
continue
|
||||
capacity_at_least = (
|
||||
other["weight_storage_saved_gb"]
|
||||
>= row["weight_storage_saved_gb"]
|
||||
)
|
||||
throughput_at_least = (
|
||||
other["throughput_median_tok_s"]
|
||||
>= row["throughput_median_tok_s"]
|
||||
)
|
||||
strictly_better = (
|
||||
other["weight_storage_saved_gb"]
|
||||
> row["weight_storage_saved_gb"]
|
||||
or other["throughput_median_tok_s"]
|
||||
> row["throughput_median_tok_s"]
|
||||
)
|
||||
if capacity_at_least and throughput_at_least and strictly_better:
|
||||
dominated_by.append(other["label"])
|
||||
row["dominated_by"] = dominated_by
|
||||
row["pareto_optimal"] = not dominated_by
|
||||
|
||||
|
||||
def aggregate_main(
|
||||
records: list[dict[str, Any]],
|
||||
metadata: dict[str, dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for record in records:
|
||||
grouped[str(record["label"])].append(record)
|
||||
missing_metadata = [label for label in CASE_ORDER if label not in metadata]
|
||||
if missing_metadata:
|
||||
raise ValueError(f"missing log metadata for: {', '.join(missing_metadata)}")
|
||||
|
||||
rows = [
|
||||
summarize_case(label, grouped[label], metadata[label])
|
||||
for label in CASE_ORDER
|
||||
]
|
||||
baseline = rows[0]
|
||||
for row in rows:
|
||||
row["throughput_change_percent"] = percent_change(
|
||||
row["throughput_median_tok_s"],
|
||||
baseline["throughput_median_tok_s"],
|
||||
)
|
||||
row["throughput_retention_percent"] = (
|
||||
row["throughput_median_tok_s"]
|
||||
/ baseline["throughput_median_tok_s"]
|
||||
* 100.0
|
||||
)
|
||||
row["pooled_round_change_percent"] = percent_change(
|
||||
row["pooled_round_median_tok_s"],
|
||||
baseline["pooled_round_median_tok_s"],
|
||||
)
|
||||
row["kv_cache_gain_gib"] = (
|
||||
row["kv_cache_available_median_gib"]
|
||||
- baseline["kv_cache_available_median_gib"]
|
||||
)
|
||||
mark_pareto(rows, baseline["label"])
|
||||
return rows
|
||||
|
||||
|
||||
def aggregate_confirmation(
|
||||
records: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for record in records:
|
||||
grouped[str(record["label"])].append(record)
|
||||
rows: list[dict[str, Any]] = []
|
||||
for label in CONFIRMATION_ORDER:
|
||||
case_records = grouped[label]
|
||||
process_rates = [
|
||||
float(record["output_tokens_per_second"]) for record in case_records
|
||||
]
|
||||
round_rates = pooled_round_throughputs(case_records)
|
||||
rows.append(
|
||||
{
|
||||
"label": label,
|
||||
"repeat_count": len(case_records),
|
||||
"throughput_runs_tok_s": process_rates,
|
||||
"throughput_median_tok_s": statistics.median(process_rates),
|
||||
"pooled_round_count": len(round_rates),
|
||||
"pooled_round_median_tok_s": statistics.median(round_rates),
|
||||
"pooled_round_mean_tok_s": statistics.mean(round_rates),
|
||||
"pooled_round_stdev_tok_s": sample_stdev(round_rates),
|
||||
}
|
||||
)
|
||||
baseline = rows[0]["pooled_round_median_tok_s"]
|
||||
for row in rows:
|
||||
row["pooled_round_change_percent"] = percent_change(
|
||||
row["pooled_round_median_tok_s"], baseline
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def validate_inputs(
|
||||
main_records: list[dict[str, Any]],
|
||||
confirmation_records: list[dict[str, Any]],
|
||||
main_logs: list[Path],
|
||||
confirmation_logs: list[Path],
|
||||
metadata_errors: list[str],
|
||||
) -> dict[str, Any]:
|
||||
main_counts = Counter(str(record.get("label")) for record in main_records)
|
||||
confirmation_counts = Counter(
|
||||
str(record.get("label")) for record in confirmation_records
|
||||
)
|
||||
all_logs = main_logs + confirmation_logs
|
||||
log_text = {
|
||||
path: path.read_text(encoding="utf-8", errors="replace")
|
||||
for path in all_logs
|
||||
}
|
||||
offload_logs = [
|
||||
path for path in all_logs if not path.name.startswith("baseline_0_1_")
|
||||
]
|
||||
validation = {
|
||||
"main_run_count": len(main_records),
|
||||
"confirmation_run_count": len(confirmation_records),
|
||||
"main_log_count": len(main_logs),
|
||||
"confirmation_log_count": len(confirmation_logs),
|
||||
"main_runs_per_label": dict(main_counts),
|
||||
"confirmation_runs_per_label": dict(confirmation_counts),
|
||||
"offload_logs_with_current_class": sum(
|
||||
"Offloader set to AscendPrefetchOffloader" in log_text[path]
|
||||
for path in offload_logs
|
||||
),
|
||||
"logs_with_ready_metadata": sum(
|
||||
"Ascend weight offloader ready:" in text
|
||||
for text in log_text.values()
|
||||
),
|
||||
"logs_with_traceback": [
|
||||
path.name
|
||||
for path, text in log_text.items()
|
||||
if "Traceback (most recent call last)" in text
|
||||
],
|
||||
"logs_without_exactly_one_bench_result": [
|
||||
path.name
|
||||
for path, text in log_text.items()
|
||||
if text.count("BENCH_RESULT ") != 1
|
||||
],
|
||||
"logs_with_custom_ops_disabled_warning": sum(
|
||||
"Failed to register custom ops" in text
|
||||
for text in log_text.values()
|
||||
),
|
||||
"metadata_errors": metadata_errors,
|
||||
}
|
||||
validation["valid"] = (
|
||||
main_counts == Counter(EXPECTED_MAIN_COUNTS)
|
||||
and confirmation_counts == Counter(EXPECTED_CONFIRMATION_COUNTS)
|
||||
and len(main_logs) == sum(EXPECTED_MAIN_COUNTS.values())
|
||||
and len(confirmation_logs) == sum(EXPECTED_CONFIRMATION_COUNTS.values())
|
||||
and validation["offload_logs_with_current_class"] == len(offload_logs)
|
||||
and validation["logs_with_ready_metadata"] == len(all_logs)
|
||||
and not validation["logs_with_traceback"]
|
||||
and not validation["logs_without_exactly_one_bench_result"]
|
||||
and not metadata_errors
|
||||
)
|
||||
return validation
|
||||
|
||||
|
||||
def display_label(label: str) -> str:
|
||||
if label == "baseline_0_1":
|
||||
return "baseline (0,1)"
|
||||
match = re.fullmatch(r"group_(\d+)_num_(\d+)", label)
|
||||
if not match:
|
||||
return label
|
||||
return f"({match.group(1)},{match.group(2)})"
|
||||
|
||||
|
||||
def write_csv(rows: list[dict[str, Any]], path: Path) -> None:
|
||||
fields = (
|
||||
"label",
|
||||
"offload_group_size",
|
||||
"offload_num_in_group",
|
||||
"offloaded_layer_count",
|
||||
"selected_layers",
|
||||
"weight_storage_saved_gb",
|
||||
"kv_cache_available_median_gib",
|
||||
"repeat_count",
|
||||
"throughput_median_tok_s",
|
||||
"throughput_change_percent",
|
||||
"throughput_retention_percent",
|
||||
"pooled_round_median_tok_s",
|
||||
"latency_p50_median_s",
|
||||
"load_time_median_s",
|
||||
"pareto_optimal",
|
||||
"dominated_by",
|
||||
)
|
||||
with path.open("w", encoding="utf-8", newline="") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=fields, extrasaction="ignore")
|
||||
writer.writeheader()
|
||||
for row in rows:
|
||||
output = dict(row)
|
||||
output["dominated_by"] = ",".join(row["dominated_by"])
|
||||
writer.writerow(output)
|
||||
|
||||
|
||||
def write_markdown(
|
||||
main_rows: list[dict[str, Any]],
|
||||
confirmation_rows: list[dict[str, Any]],
|
||||
path: Path,
|
||||
) -> None:
|
||||
lines = [
|
||||
"# v0.18 parameter optimization results",
|
||||
"",
|
||||
"| Config | Layers | Weight saved (GB) | KV cache (GiB) | Runs | "
|
||||
"Median throughput (tok/s) | Vs baseline | Retention | Pareto |",
|
||||
"|---|---:|---:|---:|---:|---:|---:|---:|---:|",
|
||||
]
|
||||
for row in main_rows:
|
||||
pareto = "-" if row["pareto_optimal"] is None else (
|
||||
"yes" if row["pareto_optimal"] else "no"
|
||||
)
|
||||
lines.append(
|
||||
f"| {display_label(row['label'])} | "
|
||||
f"{row['offloaded_layer_count']} | "
|
||||
f"{row['weight_storage_saved_gb']:.4f} | "
|
||||
f"{row['kv_cache_available_median_gib']:.2f} | "
|
||||
f"{row['repeat_count']} | "
|
||||
f"{row['throughput_median_tok_s']:.3f} | "
|
||||
f"{row['throughput_change_percent']:+.3f}% | "
|
||||
f"{row['throughput_retention_percent']:.3f}% | {pareto} |"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"## Long-window confirmation",
|
||||
"",
|
||||
"| Config | Process throughputs | Process median | "
|
||||
"30-round median | Vs baseline |",
|
||||
"|---|---:|---:|---:|---:|",
|
||||
]
|
||||
)
|
||||
for row in confirmation_rows:
|
||||
runs = " / ".join(
|
||||
f"{value:.3f}" for value in row["throughput_runs_tok_s"]
|
||||
)
|
||||
lines.append(
|
||||
f"| {display_label(row['label'])} | {runs} | "
|
||||
f"{row['throughput_median_tok_s']:.3f} | "
|
||||
f"{row['pooled_round_median_tok_s']:.3f} | "
|
||||
f"{row['pooled_round_change_percent']:+.3f}% |"
|
||||
)
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"Low-density differences are smaller than run-to-run variance and "
|
||||
"should be interpreted as equivalent throughput.",
|
||||
"",
|
||||
]
|
||||
)
|
||||
path.write_text("\n".join(lines), encoding="utf-8")
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--results-dir", type=Path, required=True)
|
||||
parser.add_argument("--branch", default="unknown")
|
||||
parser.add_argument("--commit", default="unknown")
|
||||
parser.add_argument("--vllm-version", default="unknown")
|
||||
parser.add_argument("--vllm-ascend-version", default="unknown")
|
||||
parser.add_argument("--cann-version", default="unknown")
|
||||
parser.add_argument("--hardware", default="unknown")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
root = args.results_dir
|
||||
main_records = load_jsonl(root / "performance-runs.jsonl")
|
||||
confirmation_records = load_jsonl(root / "confirmation-runs.jsonl")
|
||||
main_logs = sorted((root / "logs").glob("*.log"))
|
||||
confirmation_logs = sorted((root / "confirmation-logs").glob("*.log"))
|
||||
metadata, metadata_errors = parse_log_metadata(main_logs)
|
||||
validation = validate_inputs(
|
||||
main_records,
|
||||
confirmation_records,
|
||||
main_logs,
|
||||
confirmation_logs,
|
||||
metadata_errors,
|
||||
)
|
||||
main_rows = aggregate_main(main_records, metadata)
|
||||
confirmation_rows = aggregate_confirmation(confirmation_records)
|
||||
|
||||
first = main_records[0]
|
||||
confirmation_first = confirmation_records[0]
|
||||
payload = {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"source": {
|
||||
"branch": args.branch,
|
||||
"commit": args.commit,
|
||||
"vllm": args.vllm_version,
|
||||
"vllm_ascend": args.vllm_ascend_version,
|
||||
"cann": args.cann_version,
|
||||
},
|
||||
"hardware": args.hardware,
|
||||
"model": first["model"],
|
||||
"main_protocol": {
|
||||
"prompt_count": first["prompt_count"],
|
||||
"input_tokens": first["input_tokens"],
|
||||
"max_tokens": first["max_tokens"],
|
||||
"max_model_len": first["max_model_len"],
|
||||
"warmup_rounds": first["warmup_rounds"],
|
||||
"measured_rounds": first["measured_rounds"],
|
||||
"output_tokens_per_process": first["output_tokens"],
|
||||
"primary_metric": "per-process throughput median",
|
||||
},
|
||||
"confirmation_protocol": {
|
||||
"prompt_count": confirmation_first["prompt_count"],
|
||||
"input_tokens": confirmation_first["input_tokens"],
|
||||
"max_tokens": confirmation_first["max_tokens"],
|
||||
"max_model_len": confirmation_first["max_model_len"],
|
||||
"warmup_rounds": confirmation_first["warmup_rounds"],
|
||||
"measured_rounds": confirmation_first["measured_rounds"],
|
||||
"output_tokens_per_process": confirmation_first["output_tokens"],
|
||||
"confirmation_metric": "pooled 30-round throughput median",
|
||||
},
|
||||
"main_matrix": main_rows,
|
||||
"confirmation_matrix": confirmation_rows,
|
||||
}
|
||||
(root / "optimization-summary.json").write_text(
|
||||
json.dumps(payload, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
write_csv(main_rows, root / "optimization-summary.csv")
|
||||
write_markdown(
|
||||
main_rows, confirmation_rows, root / "optimization-summary.md"
|
||||
)
|
||||
(root / "validation.json").write_text(
|
||||
json.dumps(validation, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(json.dumps(validation, indent=2, sort_keys=True))
|
||||
if not validation["valid"]:
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
import argparse
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
_MIB_PAIR = re.compile(r"(?P<used>\d+)\s*/\s*(?P<total>\d+)")
|
||||
_DEVICE_ROW = re.compile(r"^\|\s*(?P<index>\d+)\s+\S+\s*\|")
|
||||
_DEVICE_DETAIL_ROW = re.compile(
|
||||
r"^\|\s*\d+\s*\|\s*"
|
||||
r"(?:[0-9A-Fa-f]{4}:)?[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}\.\d\s*\|"
|
||||
)
|
||||
|
||||
|
||||
def parse_npu_smi_hbm(output: str, npu_index: int) -> dict[str, int] | None:
|
||||
"""Extract HBM from a device detail row associated with its preceding NPU."""
|
||||
current_device: int | None = None
|
||||
for line in output.splitlines():
|
||||
device_match = _DEVICE_ROW.match(line)
|
||||
if device_match:
|
||||
current_device = int(device_match.group("index"))
|
||||
continue
|
||||
if "Process id" in line or "Process memory" in line:
|
||||
current_device = None
|
||||
continue
|
||||
if current_device != npu_index or not _DEVICE_DETAIL_ROW.match(line):
|
||||
continue
|
||||
for match in _MIB_PAIR.finditer(line):
|
||||
used = int(match.group("used"))
|
||||
total = int(match.group("total"))
|
||||
if total >= 1024 and used <= total:
|
||||
return {"used_mib": used, "total_mib": total}
|
||||
return None
|
||||
|
||||
|
||||
def current_rss_mib() -> float | None:
|
||||
try:
|
||||
for line in Path("/proc/self/status").read_text().splitlines():
|
||||
if line.startswith("VmRSS:"):
|
||||
return int(line.split()[1]) / 1024.0
|
||||
except (OSError, ValueError, IndexError):
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
class ResourceSampler:
|
||||
def __init__(self, npu_index: int):
|
||||
self.npu_index = npu_index
|
||||
self.samples: list[dict[str, Any]] = []
|
||||
self.rss_samples: list[float] = []
|
||||
|
||||
def _sample(self, phase: str) -> None:
|
||||
rss = current_rss_mib()
|
||||
if rss is not None:
|
||||
self.rss_samples.append(rss)
|
||||
sample: dict[str, Any] = {"timestamp": time.time(), "phase": phase}
|
||||
try:
|
||||
completed = subprocess.run(
|
||||
["npu-smi", "info"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
check=False,
|
||||
)
|
||||
raw = completed.stdout or completed.stderr
|
||||
parsed = parse_npu_smi_hbm(raw, self.npu_index)
|
||||
if parsed is None:
|
||||
sample["used_mib"] = None
|
||||
sample["total_mib"] = None
|
||||
sample["raw"] = raw[:4096]
|
||||
sample["returncode"] = completed.returncode
|
||||
else:
|
||||
sample.update(parsed)
|
||||
except (OSError, subprocess.SubprocessError) as exc:
|
||||
sample["used_mib"] = None
|
||||
sample["total_mib"] = None
|
||||
sample["raw"] = str(exc)
|
||||
self.samples.append(sample)
|
||||
|
||||
def checkpoint(self, phase: str) -> None:
|
||||
self._sample(phase)
|
||||
|
||||
def hbm_summary(self) -> tuple[int | None, int | None, int | None]:
|
||||
if not self.samples:
|
||||
return None, None, None
|
||||
parsed = [sample["used_mib"] for sample in self.samples
|
||||
if sample.get("used_mib") is not None]
|
||||
before = self.samples[0].get("used_mib")
|
||||
after = self.samples[-1].get("used_mib")
|
||||
peak = max(parsed) if parsed else None
|
||||
return before, peak, after
|
||||
|
||||
|
||||
def percentile(values: list[float], quantile: float) -> float | None:
|
||||
if not values:
|
||||
return None
|
||||
ordered = sorted(values)
|
||||
if len(ordered) == 1:
|
||||
return ordered[0]
|
||||
position = (len(ordered) - 1) * quantile
|
||||
lower = math.floor(position)
|
||||
upper = math.ceil(position)
|
||||
if lower == upper:
|
||||
return ordered[lower]
|
||||
return ordered[lower] + (ordered[upper] - ordered[lower]) * (position - lower)
|
||||
|
||||
|
||||
def build_prompts(tokenizer: Any, prompt_count: int, input_tokens: int) -> list[str]:
|
||||
seed_text = (
|
||||
"Analyze CPU weight offloading for deterministic large language model "
|
||||
"inference on Ascend hardware and summarize the practical tradeoffs. "
|
||||
)
|
||||
prompts = []
|
||||
for index in range(prompt_count):
|
||||
token_ids = tokenizer.encode(
|
||||
f"Request {index}: {seed_text}", add_special_tokens=False
|
||||
)
|
||||
if not token_ids:
|
||||
raise RuntimeError("tokenizer returned no tokens for benchmark prompt")
|
||||
repeats = math.ceil(input_tokens / len(token_ids))
|
||||
prompts.append(tokenizer.decode((token_ids * repeats)[:input_tokens]))
|
||||
return prompts
|
||||
|
||||
|
||||
def build_llm_kwargs(args: argparse.Namespace) -> dict[str, Any]:
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": args.model,
|
||||
"trust_remote_code": True,
|
||||
"dtype": "float16",
|
||||
"max_model_len": args.max_model_len,
|
||||
"max_num_seqs": args.prompt_count,
|
||||
"max_num_batched_tokens": args.max_model_len * args.prompt_count,
|
||||
"tensor_parallel_size": 1,
|
||||
"enforce_eager": True,
|
||||
"gpu_memory_utilization": args.gpu_memory_utilization,
|
||||
"seed": args.seed,
|
||||
"offload_group_size": args.offload_group_size,
|
||||
"offload_num_in_group": args.offload_num_in_group,
|
||||
}
|
||||
if getattr(args, "offload_backend", None) is not None:
|
||||
kwargs["offload_backend"] = args.offload_backend
|
||||
return kwargs
|
||||
|
||||
|
||||
def offload_result_metadata(args: argparse.Namespace) -> dict[str, str | None]:
|
||||
return {"offload_backend": getattr(args, "offload_backend", None)}
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model", required=True)
|
||||
parser.add_argument("--label", required=True)
|
||||
parser.add_argument("--offload-group-size", type=int, required=True)
|
||||
parser.add_argument("--offload-num-in-group", type=int, required=True)
|
||||
parser.add_argument("--offload-backend", choices=("auto", "prefetch", "uva"))
|
||||
parser.add_argument("--prompt-count", type=int, default=8)
|
||||
parser.add_argument("--input-tokens", type=int, default=128)
|
||||
parser.add_argument("--max-tokens", type=int, default=64)
|
||||
parser.add_argument("--max-model-len", type=int, default=512)
|
||||
parser.add_argument("--warmup-rounds", type=int, default=1)
|
||||
parser.add_argument("--measured-rounds", type=int, default=3)
|
||||
parser.add_argument("--seed", type=int, default=0)
|
||||
parser.add_argument("--result-file", type=Path, required=True)
|
||||
parser.add_argument("--npu-index", type=int, default=7)
|
||||
parser.add_argument("--gpu-memory-utilization", type=float, default=0.5)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
if min(args.prompt_count, args.input_tokens, args.max_tokens,
|
||||
args.measured_rounds) <= 0 or args.warmup_rounds < 0:
|
||||
raise ValueError("workload counts must be positive and warmup rounds non-negative")
|
||||
if not 0 < args.gpu_memory_utilization <= 1:
|
||||
raise ValueError("gpu memory utilization must be in (0, 1]")
|
||||
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
rss_before = current_rss_mib()
|
||||
sampler = ResourceSampler(args.npu_index)
|
||||
sampler.checkpoint("before_load")
|
||||
completed = False
|
||||
try:
|
||||
load_start = time.perf_counter()
|
||||
llm = LLM(**build_llm_kwargs(args))
|
||||
load_seconds = time.perf_counter() - load_start
|
||||
prompts = build_prompts(llm.get_tokenizer(), args.prompt_count,
|
||||
args.input_tokens)
|
||||
sampling = SamplingParams(
|
||||
temperature=0,
|
||||
max_tokens=args.max_tokens,
|
||||
seed=args.seed,
|
||||
)
|
||||
|
||||
for _ in range(args.warmup_rounds):
|
||||
llm.generate(prompts, sampling)
|
||||
|
||||
round_latencies: list[float] = []
|
||||
output_tokens = 0
|
||||
generation_start = time.perf_counter()
|
||||
for _ in range(args.measured_rounds):
|
||||
round_start = time.perf_counter()
|
||||
outputs = llm.generate(prompts, sampling)
|
||||
round_latencies.append(time.perf_counter() - round_start)
|
||||
output_tokens += sum(
|
||||
len(request.outputs[0].token_ids) for request in outputs
|
||||
)
|
||||
generate_seconds = time.perf_counter() - generation_start
|
||||
completed = True
|
||||
finally:
|
||||
if completed:
|
||||
sampler.checkpoint("after_generation")
|
||||
|
||||
rss_after = current_rss_mib()
|
||||
rss_values = [value for value in [rss_before, *sampler.rss_samples, rss_after]
|
||||
if value is not None]
|
||||
hbm_before, hbm_peak, hbm_after = sampler.hbm_summary()
|
||||
request_count = args.prompt_count * args.measured_rounds
|
||||
result = {
|
||||
"label": args.label,
|
||||
"model": args.model,
|
||||
"seed": args.seed,
|
||||
**offload_result_metadata(args),
|
||||
"offload_group_size": args.offload_group_size,
|
||||
"offload_num_in_group": args.offload_num_in_group,
|
||||
"prompt_count": args.prompt_count,
|
||||
"input_tokens": args.input_tokens,
|
||||
"max_tokens": args.max_tokens,
|
||||
"max_model_len": args.max_model_len,
|
||||
"gpu_memory_utilization": args.gpu_memory_utilization,
|
||||
"warmup_rounds": args.warmup_rounds,
|
||||
"measured_rounds": args.measured_rounds,
|
||||
"npu_index": args.npu_index,
|
||||
"load_seconds": load_seconds,
|
||||
"generate_seconds": generate_seconds,
|
||||
"request_count": request_count,
|
||||
"output_tokens": output_tokens,
|
||||
"output_tokens_per_second": output_tokens / generate_seconds,
|
||||
"requests_per_second": request_count / generate_seconds,
|
||||
"round_latencies_seconds": round_latencies,
|
||||
"latency_p50_seconds": percentile(round_latencies, 0.50),
|
||||
"latency_p95_seconds": percentile(round_latencies, 0.95),
|
||||
"process_rss_before_mib": rss_before,
|
||||
"process_rss_peak_mib": max(rss_values) if rss_values else None,
|
||||
"process_rss_after_mib": rss_after,
|
||||
"npu_hbm_before_mib": hbm_before,
|
||||
"npu_hbm_peak_mib": hbm_peak,
|
||||
"npu_hbm_after_mib": hbm_after,
|
||||
"npu_hbm_samples": sampler.samples,
|
||||
"pid": os.getpid(),
|
||||
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
}
|
||||
args.result_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with args.result_file.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(result, sort_keys=True) + "\n")
|
||||
print("BENCH_RESULT", json.dumps(result, sort_keys=True), flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
import argparse
|
||||
import csv
|
||||
import json
|
||||
import statistics
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
MEDIAN_FIELDS = (
|
||||
"load_seconds",
|
||||
"generate_seconds",
|
||||
"request_count",
|
||||
"output_tokens",
|
||||
"output_tokens_per_second",
|
||||
"requests_per_second",
|
||||
"latency_p50_seconds",
|
||||
"latency_p95_seconds",
|
||||
"process_rss_before_mib",
|
||||
"process_rss_peak_mib",
|
||||
"process_rss_after_mib",
|
||||
"npu_hbm_before_mib",
|
||||
"npu_hbm_peak_mib",
|
||||
"npu_hbm_after_mib",
|
||||
)
|
||||
|
||||
|
||||
def percent_change(value: float | None,
|
||||
baseline: float | None) -> float | None:
|
||||
if value is None or baseline is None or baseline == 0:
|
||||
return None
|
||||
return (value - baseline) / baseline * 100.0
|
||||
|
||||
|
||||
def hbm_saved_gib(baseline_mib: float | None,
|
||||
value_mib: float | None) -> float | None:
|
||||
if baseline_mib is None or value_mib is None:
|
||||
return None
|
||||
return (baseline_mib - value_mib) / 1024.0
|
||||
|
||||
|
||||
def median_or_none(records: list[dict[str, Any]], field: str) -> float | None:
|
||||
values = [record[field] for record in records
|
||||
if isinstance(record.get(field), (int, float))]
|
||||
return statistics.median(values) if values else None
|
||||
|
||||
|
||||
def aggregate_records(records: list[dict[str, Any]],
|
||||
baseline_label: str) -> list[dict[str, Any]]:
|
||||
grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
||||
for record in records:
|
||||
label = record.get("label")
|
||||
if not isinstance(label, str) or not label:
|
||||
raise ValueError("every benchmark record must have a non-empty label")
|
||||
grouped[label].append(record)
|
||||
if baseline_label not in grouped:
|
||||
raise ValueError(f"baseline label {baseline_label!r} is missing")
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
for label in sorted(grouped, key=lambda item: (item != baseline_label, item)):
|
||||
case_records = grouped[label]
|
||||
first = case_records[0]
|
||||
row: dict[str, Any] = {
|
||||
"label": label,
|
||||
"repeats": len(case_records),
|
||||
"model": first.get("model"),
|
||||
"offload_group_size": first.get("offload_group_size"),
|
||||
"offload_num_in_group": first.get("offload_num_in_group"),
|
||||
}
|
||||
for field in MEDIAN_FIELDS:
|
||||
row[field] = median_or_none(case_records, field)
|
||||
rows.append(row)
|
||||
|
||||
baseline = next(row for row in rows if row["label"] == baseline_label)
|
||||
for row in rows:
|
||||
if row["label"] == baseline_label:
|
||||
row["throughput_change_percent"] = 0.0
|
||||
row["hbm_saved_gib"] = 0.0
|
||||
else:
|
||||
row["throughput_change_percent"] = percent_change(
|
||||
row["output_tokens_per_second"],
|
||||
baseline["output_tokens_per_second"],
|
||||
)
|
||||
row["hbm_saved_gib"] = hbm_saved_gib(
|
||||
baseline["npu_hbm_peak_mib"], row["npu_hbm_peak_mib"]
|
||||
)
|
||||
return rows
|
||||
|
||||
|
||||
def load_jsonl(path: Path) -> list[dict[str, Any]]:
|
||||
records = []
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
for line_number, line in enumerate(handle, 1):
|
||||
if not line.strip():
|
||||
continue
|
||||
try:
|
||||
record = json.loads(line)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"invalid JSON on {path}:{line_number}: {exc}") from exc
|
||||
if not isinstance(record, dict):
|
||||
raise ValueError(f"record on {path}:{line_number} is not an object")
|
||||
records.append(record)
|
||||
if not records:
|
||||
raise ValueError(f"no benchmark records found in {path}")
|
||||
return records
|
||||
|
||||
|
||||
def format_cell(value: Any) -> str:
|
||||
if value is None:
|
||||
return "N/A"
|
||||
if isinstance(value, float):
|
||||
return f"{value:.4f}"
|
||||
return str(value)
|
||||
|
||||
|
||||
def write_outputs(rows: list[dict[str, Any]], output_dir: Path,
|
||||
baseline_label: str, input_path: Path) -> None:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
fieldnames = list(rows[0])
|
||||
csv_path = output_dir / "performance-summary.csv"
|
||||
with csv_path.open("w", encoding="utf-8", newline="") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
markdown_fields = (
|
||||
"label", "repeats", "output_tokens_per_second", "requests_per_second",
|
||||
"latency_p50_seconds", "latency_p95_seconds", "npu_hbm_peak_mib",
|
||||
"throughput_change_percent", "hbm_saved_gib",
|
||||
)
|
||||
lines = [
|
||||
"| " + " | ".join(markdown_fields) + " |",
|
||||
"| " + " | ".join("---" for _ in markdown_fields) + " |",
|
||||
]
|
||||
lines.extend(
|
||||
"| " + " | ".join(format_cell(row[field]) for field in markdown_fields) + " |"
|
||||
for row in rows
|
||||
)
|
||||
(output_dir / "performance-summary.md").write_text(
|
||||
"\n".join(lines) + "\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
payload = {
|
||||
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"baseline_label": baseline_label,
|
||||
"input_jsonl": str(input_path.resolve()),
|
||||
"aggregation": "median",
|
||||
"rows": rows,
|
||||
}
|
||||
(output_dir / "performance-summary.json").write_text(
|
||||
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--input", type=Path, required=True)
|
||||
parser.add_argument("--output-dir", type=Path, required=True)
|
||||
parser.add_argument("--baseline-label", default="baseline_0_1")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
rows = aggregate_records(load_jsonl(args.input), args.baseline_label)
|
||||
write_outputs(rows, args.output_dir, args.baseline_label, args.input)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
NPU_INDEX=${NPU_INDEX:-$("${PYTHON:-python3}" "${SCRIPT_DIR}/detect_npu_index.py")}
|
||||
NPU_CHIP_INDEX=${NPU_CHIP_INDEX:-0}
|
||||
|
||||
set +e
|
||||
health_output=$(npu-smi info -t health -i "${NPU_INDEX}" -c "${NPU_CHIP_INDEX}" 2>&1)
|
||||
status=$?
|
||||
set -e
|
||||
printf '%s\n' "${health_output}"
|
||||
|
||||
if [[ ${status} -ne 0 ]] || ! grep -Eq 'Health Status[[:space:]]*:[[:space:]]*OK' <<<"${health_output}"; then
|
||||
echo "NPU ${NPU_INDEX}/${NPU_CHIP_INDEX} is not healthy; aborting before vLLM startup." >&2
|
||||
exit 42
|
||||
fi
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
|
||||
|
||||
def parse_mapping(output):
|
||||
for line in output.splitlines():
|
||||
fields = line.split()
|
||||
if (
|
||||
len(fields) >= 4
|
||||
and fields[0].isdigit()
|
||||
and fields[1].isdigit()
|
||||
and (fields[2].isdigit() or fields[2] == "-")
|
||||
and fields[3].startswith("Ascend")
|
||||
):
|
||||
return int(fields[0])
|
||||
raise RuntimeError("unable to find an Ascend NPU mapping in npu-smi output")
|
||||
|
||||
|
||||
def detect_npu_index():
|
||||
completed = subprocess.run(
|
||||
["npu-smi", "info", "-m"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return parse_mapping(completed.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(detect_npu_index())
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT=${REPO_ROOT:-/data/vllm-ascend-v0.18.0}
|
||||
CANN_ENV=${CANN_ENV:-/usr/local/Ascend/cann-8.5.1/cann-8.5.1/set_env.sh}
|
||||
VENV_ACTIVATE=${VENV_ACTIVATE:-/data/vllm-ascend-v0.18.0/.venv/bin/activate}
|
||||
INSTALLER_DIR=${INSTALLER_DIR:-/data/installers/cann-8.5.1}
|
||||
TORCH_NPU_INDEX=${TORCH_NPU_INDEX:-https://mirrors.huaweicloud.com/repository/pypi/simple}
|
||||
TRITON_WHEEL_NAME=triton_ascend-3.2.0.dev20260322-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
|
||||
TRITON_WHEEL_URL=${TRITON_WHEEL_URL:-https://vllm-ascend.obs.cn-north-4.myhuaweicloud.com/vllm-ascend/${TRITON_WHEEL_NAME}}
|
||||
TRITON_WHEEL=${INSTALLER_DIR}/${TRITON_WHEEL_NAME}
|
||||
|
||||
if [[ "$(realpath "${REPO_ROOT}")" != /data/vllm-ascend-v0.18.0 ]]; then
|
||||
echo "REPO_ROOT must resolve to /data/vllm-ascend-v0.18.0" >&2
|
||||
exit 2
|
||||
fi
|
||||
[[ -f "${CANN_ENV}" ]]
|
||||
[[ -f "${VENV_ACTIVATE}" ]]
|
||||
|
||||
source "${CANN_ENV}"
|
||||
source "${VENV_ACTIVATE}"
|
||||
mkdir -p "${INSTALLER_DIR}"
|
||||
|
||||
python -m pip install \
|
||||
--no-deps \
|
||||
--force-reinstall \
|
||||
--index-url "${TORCH_NPU_INDEX}" \
|
||||
torch-npu==2.9.0
|
||||
|
||||
curl --fail --location --continue-at - \
|
||||
--output "${TRITON_WHEEL}" "${TRITON_WHEEL_URL}"
|
||||
python -m pip uninstall -y triton triton-ascend || true
|
||||
python -m pip install --no-deps "${TRITON_WHEEL}"
|
||||
|
||||
# Rebuild all CANN-bound artifacts after switching toolkit versions.
|
||||
rm -rf \
|
||||
"${REPO_ROOT}/csrc/build" \
|
||||
"${REPO_ROOT}/build" \
|
||||
"${REPO_ROOT}/vllm_ascend/_cann_ops_custom"
|
||||
rm -f \
|
||||
"${REPO_ROOT}"/vllm_ascend/vllm_ascend_C*.so \
|
||||
"${REPO_ROOT}"/vllm_ascend/libvllm_ascend_kernels.so
|
||||
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export CMAKE_ARGS="-DPython_EXECUTABLE=${VIRTUAL_ENV}/bin/python -DPython_ROOT_DIR=${VIRTUAL_ENV}"
|
||||
python -m pip install --no-build-isolation --no-deps -e "${REPO_ROOT}"
|
||||
|
||||
python - <<'PY'
|
||||
import importlib.metadata
|
||||
|
||||
triton_version = importlib.metadata.version("triton-ascend")
|
||||
torch_npu_version = importlib.metadata.version("torch-npu")
|
||||
if triton_version != "3.2.0.dev20260322":
|
||||
raise RuntimeError(
|
||||
f"expected triton-ascend 3.2.0.dev20260322, got {triton_version}"
|
||||
)
|
||||
if torch_npu_version != "2.9.0":
|
||||
raise RuntimeError(f"expected torch-npu 2.9.0, got {torch_npu_version}")
|
||||
print(f"triton-ascend={triton_version}")
|
||||
print(f"torch-npu={torch_npu_version}")
|
||||
PY
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
_EVIDENCE_PATH = os.environ.get("OFFLOAD_EVIDENCE_PATH")
|
||||
_EVIDENCE_MODE = os.environ.get("OFFLOAD_EVIDENCE_MODE")
|
||||
|
||||
|
||||
def _publish_evidence(payload, allow_identical):
|
||||
if not _EVIDENCE_PATH:
|
||||
raise RuntimeError("OFFLOAD_EVIDENCE_PATH is required")
|
||||
serialized = json.dumps(payload, sort_keys=True, separators=(",", ":"))
|
||||
expected_contents = serialized + "\n"
|
||||
target = Path(_EVIDENCE_PATH)
|
||||
fd, temporary_name = tempfile.mkstemp(
|
||||
prefix=f".{target.name}.",
|
||||
suffix=".tmp",
|
||||
dir=target.parent,
|
||||
)
|
||||
temporary = Path(temporary_name)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
handle.write(expected_contents)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
try:
|
||||
os.link(temporary, target)
|
||||
except FileExistsError as exc:
|
||||
if (
|
||||
allow_identical
|
||||
and target.read_text(encoding="utf-8") == expected_contents
|
||||
):
|
||||
return False
|
||||
raise RuntimeError(
|
||||
"runtime evidence path already contains different evidence"
|
||||
) from exc
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
print("OFFLOAD_RUNTIME_EVIDENCE " + serialized, flush=True)
|
||||
return True
|
||||
|
||||
|
||||
def _write_evidence(payload):
|
||||
_publish_evidence(payload, allow_identical=False)
|
||||
|
||||
|
||||
def _baseline_evidence():
|
||||
return {
|
||||
"schema_version": 1,
|
||||
"hook_loaded": True,
|
||||
"offloader_class": None,
|
||||
"group_size": 0,
|
||||
"num_in_group": 1,
|
||||
"selected_layers": [],
|
||||
"wrapped_count": 0,
|
||||
"offloaded_bytes": 0,
|
||||
}
|
||||
|
||||
|
||||
def _patch_offloader_class(offloader_class):
|
||||
original_wrap_modules = offloader_class.wrap_modules
|
||||
original_post_init = offloader_class.post_init
|
||||
|
||||
def wrap_modules(self, modules_generator):
|
||||
observed_modules = []
|
||||
|
||||
def observe_modules():
|
||||
for module in modules_generator:
|
||||
observed_modules.append(module)
|
||||
yield module
|
||||
|
||||
result = original_wrap_modules(self, observe_modules())
|
||||
indices_by_identity = {
|
||||
id(module): index for index, module in enumerate(observed_modules)
|
||||
}
|
||||
selected_layers = []
|
||||
for module_offloader in self.module_offloaders:
|
||||
module_identity = id(module_offloader.module)
|
||||
if module_identity not in indices_by_identity:
|
||||
raise RuntimeError(
|
||||
"offloader wrapped a module not yielded by wrap_modules"
|
||||
)
|
||||
selected_layers.append(indices_by_identity[module_identity])
|
||||
self._qwen_actual_selected_layers = tuple(selected_layers)
|
||||
return result
|
||||
|
||||
def post_init(self, *args, **kwargs):
|
||||
result = original_post_init(self, *args, **kwargs)
|
||||
selected_layers = getattr(self, "_qwen_actual_selected_layers", None)
|
||||
if selected_layers is None:
|
||||
raise RuntimeError("wrap_modules evidence was not captured")
|
||||
offloaded_bytes = getattr(self, "total_offloaded_bytes", None)
|
||||
if (
|
||||
isinstance(offloaded_bytes, bool)
|
||||
or not isinstance(offloaded_bytes, int)
|
||||
or offloaded_bytes < 0
|
||||
):
|
||||
raise RuntimeError("offloader did not expose integer offloaded bytes")
|
||||
_write_evidence({
|
||||
"schema_version": 1,
|
||||
"hook_loaded": True,
|
||||
"offloader_class": (
|
||||
f"{type(self).__module__}.{type(self).__name__}"
|
||||
),
|
||||
"group_size": self.group_size,
|
||||
"num_in_group": self.num_in_group,
|
||||
"selected_layers": list(selected_layers),
|
||||
"wrapped_count": len(self.module_offloaders),
|
||||
"offloaded_bytes": offloaded_bytes,
|
||||
})
|
||||
return result
|
||||
|
||||
offloader_class.wrap_modules = wrap_modules
|
||||
offloader_class.post_init = post_init
|
||||
|
||||
|
||||
def _install():
|
||||
from vllm_ascend.model_executor.offloader import prefetch
|
||||
|
||||
patched_class_ids = set()
|
||||
for class_name in ("AscendPrefetchOffloader", "NPUPrefetchOffloader"):
|
||||
offloader_class = getattr(prefetch, class_name, None)
|
||||
if offloader_class is None or id(offloader_class) in patched_class_ids:
|
||||
continue
|
||||
_patch_offloader_class(offloader_class)
|
||||
patched_class_ids.add(id(offloader_class))
|
||||
if not patched_class_ids:
|
||||
raise RuntimeError("no supported prefetch offloader class was found")
|
||||
|
||||
|
||||
if _EVIDENCE_PATH:
|
||||
if _EVIDENCE_MODE == "baseline":
|
||||
_publish_evidence(_baseline_evidence(), allow_identical=True)
|
||||
elif _EVIDENCE_MODE == "actual":
|
||||
_install()
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"OFFLOAD_EVIDENCE_MODE must be baseline or actual"
|
||||
)
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CANN_ENV=${CANN_ENV:-/usr/local/Ascend/cann-8.5.1/cann-8.5.1/set_env.sh}
|
||||
VENV_ACTIVATE=${VENV_ACTIVATE:-/data/vllm-ascend-v0.18.0/.venv/bin/activate}
|
||||
source "${CANN_ENV}"
|
||||
source "${VENV_ACTIVATE}"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
VLLM_ASCEND_CUSTOM_OP_ROOT=${VLLM_ASCEND_CUSTOM_OP_ROOT-/data/vllm-ascend-v0.18.0/vllm_ascend/_cann_ops_custom/vendors/vllm-ascend}
|
||||
if [[ -n "${VLLM_ASCEND_CUSTOM_OP_ROOT}" ]]; then
|
||||
export ASCEND_CUSTOM_OPP_PATH="${VLLM_ASCEND_CUSTOM_OP_ROOT}${ASCEND_CUSTOM_OPP_PATH:+:${ASCEND_CUSTOM_OPP_PATH}}"
|
||||
export LD_LIBRARY_PATH="${VLLM_ASCEND_CUSTOM_OP_ROOT}/op_api/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
||||
fi
|
||||
export CANN_ENV VENV_ACTIVATE VLLM_ASCEND_CUSTOM_OP_ROOT
|
||||
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export VLLM_USE_MODELSCOPE=True
|
||||
export ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
export NPU_INDEX=${NPU_INDEX:-$(python "${SCRIPT_DIR}/detect_npu_index.py")}
|
||||
export NPU_CHIP_INDEX=${NPU_CHIP_INDEX:-0}
|
||||
export MODELSCOPE_CACHE=${MODELSCOPE_CACHE:-/data/modelscope-cache}
|
||||
export HF_HOME=${HF_HOME:-/data/huggingface-cache}
|
||||
export TRITON_CACHE_DIR=${TRITON_CACHE_DIR:-/data/triton-cache-cann851}
|
||||
|
||||
MODEL_PATH=${MODEL_PATH:?MODEL_PATH must point to the model}
|
||||
DATA_DIR=${DATA_DIR:-/data/accuracy-gsm8k-gpqa}
|
||||
RESULTS_DIR=${RESULTS_DIR:-/data/accuracy-results}
|
||||
ACCURACY_LIMIT=${ACCURACY_LIMIT:-0}
|
||||
SEED=${SEED:-0}
|
||||
DATASET_REVISION=${DATASET_REVISION:-master}
|
||||
MAX_MODEL_LEN=${MAX_MODEL_LEN:-4096}
|
||||
GPU_MEMORY_UTILIZATION=${GPU_MEMORY_UTILIZATION:-0.5}
|
||||
GSM8K_MAX_TOKENS=${GSM8K_MAX_TOKENS:-512}
|
||||
GPQA_MAX_TOKENS=${GPQA_MAX_TOKENS:-8}
|
||||
MAX_ACCURACY_DROP=${MAX_ACCURACY_DROP:-0}
|
||||
OFFLOAD_BACKEND=${OFFLOAD_BACKEND:-}
|
||||
|
||||
if [[ ${SKIP_NPU_HEALTH_CHECK:-0} != 1 ]]; then
|
||||
bash "${SCRIPT_DIR}/check_npu_health.sh"
|
||||
fi
|
||||
|
||||
run_case() {
|
||||
local case_name=$1
|
||||
local group_size=$2
|
||||
local num_in_group=$3
|
||||
local case_dir="${RESULTS_DIR}/${case_name}"
|
||||
local offload_backend_args=()
|
||||
if [[ "$group_size" -gt 0 && -n "$OFFLOAD_BACKEND" ]]; then
|
||||
offload_backend_args=(--offload-backend "$OFFLOAD_BACKEND")
|
||||
fi
|
||||
|
||||
python "${REPO_ROOT}/scripts/accuracy_gsm8k_gpqa.py" \
|
||||
--model "$MODEL_PATH" --dataset gsm8k --data-dir "$DATA_DIR" --output-dir "$case_dir" \
|
||||
--limit "$ACCURACY_LIMIT" --seed "$SEED" --dataset-revision "$DATASET_REVISION" \
|
||||
--max-model-len "$MAX_MODEL_LEN" \
|
||||
--gpu-memory-utilization "$GPU_MEMORY_UTILIZATION" \
|
||||
--max-tokens "$GSM8K_MAX_TOKENS" --offload-group-size "$group_size" \
|
||||
--offload-num-in-group "$num_in_group" "${offload_backend_args[@]}"
|
||||
python "${REPO_ROOT}/scripts/accuracy_gsm8k_gpqa.py" \
|
||||
--model "$MODEL_PATH" --dataset gpqa --data-dir "$DATA_DIR" --output-dir "$case_dir" \
|
||||
--limit "$ACCURACY_LIMIT" --seed "$SEED" --dataset-revision "$DATASET_REVISION" \
|
||||
--max-model-len "$MAX_MODEL_LEN" \
|
||||
--gpu-memory-utilization "$GPU_MEMORY_UTILIZATION" \
|
||||
--max-tokens "$GPQA_MAX_TOKENS" --offload-group-size "$group_size" \
|
||||
--offload-num-in-group "$num_in_group" "${offload_backend_args[@]}"
|
||||
}
|
||||
|
||||
run_case "baseline_0_1" 0 1
|
||||
run_case "group_8_num_1" 8 1
|
||||
run_case "group_8_num_2" 8 2
|
||||
|
||||
VERIFY_MODE_ARGS=()
|
||||
if [[ "$ACCURACY_LIMIT" != 0 ]]; then
|
||||
VERIFY_MODE_ARGS+=(--allow-limited)
|
||||
fi
|
||||
if [[ -n "$OFFLOAD_BACKEND" ]]; then
|
||||
VERIFY_MODE_ARGS+=(--expected-positive-backend "$OFFLOAD_BACKEND")
|
||||
fi
|
||||
|
||||
python "${REPO_ROOT}/scripts/verify_accuracy_matrix.py" \
|
||||
--results-dir "$RESULTS_DIR" \
|
||||
--max-accuracy-drop "$MAX_ACCURACY_DROP" \
|
||||
"${VERIFY_MODE_ARGS[@]}"
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CANN_ENV=${CANN_ENV:-/usr/local/Ascend/cann-8.5.1/cann-8.5.1/set_env.sh}
|
||||
VENV_ACTIVATE=${VENV_ACTIVATE:-/data/vllm-ascend-v0.18.0/.venv/bin/activate}
|
||||
source "${CANN_ENV}"
|
||||
source "${VENV_ACTIVATE}"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
VLLM_ASCEND_CUSTOM_OP_ROOT=${VLLM_ASCEND_CUSTOM_OP_ROOT-/data/vllm-ascend-v0.18.0/vllm_ascend/_cann_ops_custom/vendors/vllm-ascend}
|
||||
if [[ -n "${VLLM_ASCEND_CUSTOM_OP_ROOT}" ]]; then
|
||||
export ASCEND_CUSTOM_OPP_PATH="${VLLM_ASCEND_CUSTOM_OP_ROOT}${ASCEND_CUSTOM_OPP_PATH:+:${ASCEND_CUSTOM_OPP_PATH}}"
|
||||
export LD_LIBRARY_PATH="${VLLM_ASCEND_CUSTOM_OP_ROOT}/op_api/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
||||
fi
|
||||
export CANN_ENV VENV_ACTIVATE VLLM_ASCEND_CUSTOM_OP_ROOT
|
||||
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export VLLM_USE_MODELSCOPE=True
|
||||
export ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
export NPU_INDEX=${NPU_INDEX:-$(python "${SCRIPT_DIR}/detect_npu_index.py")}
|
||||
export NPU_CHIP_INDEX=${NPU_CHIP_INDEX:-0}
|
||||
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-/data/cache}
|
||||
export HF_HOME=${HF_HOME:-/data/huggingface-cache}
|
||||
export MODELSCOPE_CACHE=${MODELSCOPE_CACHE:-/data/modelscope-cache}
|
||||
export VLLM_CACHE_ROOT=${VLLM_CACHE_ROOT:-/data/vllm-cache}
|
||||
export PYTORCH_NPU_ALLOC_CONF=${PYTORCH_NPU_ALLOC_CONF:-expandable_segments:True}
|
||||
export TRITON_CACHE_DIR=${TRITON_CACHE_DIR:-/data/triton-cache-cann851}
|
||||
|
||||
MODEL_PATH=${MODEL_PATH:?MODEL_PATH must point to the model}
|
||||
RESULTS_DIR=${RESULTS_DIR:-/data/performance-results}
|
||||
REPEATS=${REPEATS:-3}
|
||||
WARMUP_ROUNDS=${WARMUP_ROUNDS:-1}
|
||||
MEASURED_ROUNDS=${MEASURED_ROUNDS:-3}
|
||||
PROMPT_COUNT=${PROMPT_COUNT:-8}
|
||||
INPUT_TOKENS=${INPUT_TOKENS:-128}
|
||||
MAX_MODEL_LEN=${MAX_MODEL_LEN:-512}
|
||||
MAX_TOKENS=${MAX_TOKENS:-64}
|
||||
GPU_MEMORY_UTILIZATION=${GPU_MEMORY_UTILIZATION:-0.5}
|
||||
SEED=${SEED:-0}
|
||||
OFFLOAD_BACKEND=${OFFLOAD_BACKEND:-}
|
||||
|
||||
if [[ ${SKIP_NPU_HEALTH_CHECK:-0} != 1 ]]; then
|
||||
bash "${SCRIPT_DIR}/check_npu_health.sh"
|
||||
fi
|
||||
|
||||
mkdir -p "${RESULTS_DIR}"
|
||||
RAW_RESULTS="${RESULTS_DIR}/performance-runs.jsonl"
|
||||
RUN_LOG="${RESULTS_DIR}/performance-run.log"
|
||||
: > "${RAW_RESULTS}"
|
||||
: > "${RUN_LOG}"
|
||||
|
||||
CASES=(
|
||||
"baseline_0_1:0:1"
|
||||
"group_8_num_1:8:1"
|
||||
"group_8_num_2:8:2"
|
||||
)
|
||||
|
||||
for ((repeat=0; repeat<REPEATS; repeat++)); do
|
||||
start=$((repeat % ${#CASES[@]}))
|
||||
for ((offset=0; offset<${#CASES[@]}; offset++)); do
|
||||
index=$(((start + offset) % ${#CASES[@]}))
|
||||
IFS=: read -r label group_size num_in_group <<< "${CASES[index]}"
|
||||
OFFLOAD_BACKEND_ARGS=()
|
||||
if [[ "$group_size" -gt 0 && -n "$OFFLOAD_BACKEND" ]]; then
|
||||
OFFLOAD_BACKEND_ARGS=(--offload-backend "$OFFLOAD_BACKEND")
|
||||
fi
|
||||
echo "repeat=${repeat} case=${label}" | tee -a "${RUN_LOG}"
|
||||
python "${SCRIPT_DIR}/bench_one.py" \
|
||||
--model "${MODEL_PATH}" \
|
||||
--label "${label}" \
|
||||
--offload-group-size "${group_size}" \
|
||||
--offload-num-in-group "${num_in_group}" "${OFFLOAD_BACKEND_ARGS[@]}" \
|
||||
--prompt-count "${PROMPT_COUNT}" \
|
||||
--input-tokens "${INPUT_TOKENS}" \
|
||||
--max-tokens "${MAX_TOKENS}" \
|
||||
--max-model-len "${MAX_MODEL_LEN}" \
|
||||
--warmup-rounds "${WARMUP_ROUNDS}" \
|
||||
--measured-rounds "${MEASURED_ROUNDS}" \
|
||||
--seed "${SEED}" \
|
||||
--result-file "${RAW_RESULTS}" \
|
||||
--gpu-memory-utilization "${GPU_MEMORY_UTILIZATION}" \
|
||||
--npu-index "${NPU_INDEX}" 2>&1 | tee -a "${RUN_LOG}"
|
||||
done
|
||||
done
|
||||
|
||||
python "${SCRIPT_DIR}/bench_param_matrix.py" \
|
||||
--input "${RAW_RESULTS}" \
|
||||
--output-dir "${RESULTS_DIR}" \
|
||||
--baseline-label baseline_0_1
|
||||
|
|
@ -0,0 +1,260 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CANN_ENV=${CANN_ENV:-/data/Ascend/cann/set_env.sh}
|
||||
ATB_ENV=${ATB_ENV:-/data/Ascend/nnal/atb/set_env.sh}
|
||||
VENV_ACTIVATE=${VENV_ACTIVATE:-/data/vllm-ascend-v0.23.0rc1/.venv-cann901/bin/activate}
|
||||
|
||||
source "${CANN_ENV}"
|
||||
if [[ -f "${ATB_ENV}" ]]; then
|
||||
source "${ATB_ENV}"
|
||||
fi
|
||||
source "${VENV_ACTIVATE}"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
MODEL_PATH=${MODEL_PATH:?MODEL_PATH must point to the model}
|
||||
RESULTS_DIR=${RESULTS_DIR:-/data/current-vs-latest-performance}
|
||||
CURRENT_OVERLAY_ROOT=${CURRENT_OVERLAY_ROOT:-"${RESULTS_DIR}.current-impl-overlay"}
|
||||
COOLDOWN_SECONDS=${COOLDOWN_SECONDS:-3}
|
||||
NPU_INDEX=${NPU_INDEX:-6}
|
||||
NPU_CHIP_INDEX=${NPU_CHIP_INDEX:-0}
|
||||
ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
|
||||
PROMPT_COUNT=${PROMPT_COUNT:-8}
|
||||
INPUT_TOKENS=${INPUT_TOKENS:-128}
|
||||
MAX_TOKENS=${MAX_TOKENS:-64}
|
||||
MAX_MODEL_LEN=${MAX_MODEL_LEN:-512}
|
||||
WARMUP_ROUNDS=${WARMUP_ROUNDS:-1}
|
||||
MEASURED_ROUNDS=${MEASURED_ROUNDS:-3}
|
||||
GPU_MEMORY_UTILIZATION=${GPU_MEMORY_UTILIZATION:-0.5}
|
||||
SEED=${SEED:-0}
|
||||
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export ASCEND_RT_VISIBLE_DEVICES
|
||||
export NPU_INDEX NPU_CHIP_INDEX
|
||||
export SOC_VERSION=${SOC_VERSION:-ascend910b1}
|
||||
export TASK_QUEUE_ENABLE=${TASK_QUEUE_ENABLE:-1}
|
||||
export OMP_NUM_THREADS=${OMP_NUM_THREADS:-1}
|
||||
export PYTORCH_NPU_ALLOC_CONF=${PYTORCH_NPU_ALLOC_CONF:-expandable_segments:True}
|
||||
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-/data/cache-v023}
|
||||
export HF_HOME=${HF_HOME:-/data/huggingface-cache}
|
||||
export MODELSCOPE_CACHE=${MODELSCOPE_CACHE:-/data/modelscope-cache}
|
||||
export VLLM_CACHE_ROOT=${VLLM_CACHE_ROOT:-/data/vllm-cache-v023}
|
||||
export TRITON_CACHE_DIR=${TRITON_CACHE_DIR:-/data/triton-cache-cann901}
|
||||
export PYTHONNOUSERSITE=1
|
||||
|
||||
if [[ ${SKIP_NPU_HEALTH_CHECK:-0} != 1 ]]; then
|
||||
bash "${SCRIPT_DIR}/check_npu_health.sh"
|
||||
fi
|
||||
|
||||
if [[ -e "${RESULTS_DIR}/performance-runs.jsonl" ]]; then
|
||||
echo "RESULTS_DIR already contains performance-runs.jsonl: ${RESULTS_DIR}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -e "${CURRENT_OVERLAY_ROOT}" ]]; then
|
||||
echo "CURRENT_OVERLAY_ROOT already exists: ${CURRENT_OVERLAY_ROOT}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p "${RESULTS_DIR}/logs" "${CURRENT_OVERLAY_ROOT}"
|
||||
|
||||
OFFICIAL_PACKAGE_DIR="$(
|
||||
python - <<'PY'
|
||||
from importlib.metadata import distribution
|
||||
|
||||
print(distribution("vllm_ascend").locate_file("vllm_ascend"))
|
||||
PY
|
||||
)"
|
||||
if [[ ! -d "${OFFICIAL_PACKAGE_DIR}" ]]; then
|
||||
echo "Installed vllm_ascend package was not found: ${OFFICIAL_PACKAGE_DIR}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Keep the latest runner and replace only the offloader under test.
|
||||
cp -a "${OFFICIAL_PACKAGE_DIR}" "${CURRENT_OVERLAY_ROOT}/vllm_ascend"
|
||||
cp -a \
|
||||
"${REPO_ROOT}/vllm_ascend/model_executor/offloader/." \
|
||||
"${CURRENT_OVERLAY_ROOT}/vllm_ascend/model_executor/offloader/"
|
||||
|
||||
CURRENT_PREFETCH="$(
|
||||
printf '%s' \
|
||||
"${CURRENT_OVERLAY_ROOT}/vllm_ascend/model_executor/offloader/prefetch.py"
|
||||
)"
|
||||
if ! grep -qx 'NPUPrefetchOffloader = AscendPrefetchOffloader' \
|
||||
"${CURRENT_PREFETCH}"; then
|
||||
printf '\n# v0.23 runner compatibility name.\nNPUPrefetchOffloader = AscendPrefetchOffloader\n' \
|
||||
>> "${CURRENT_PREFETCH}"
|
||||
fi
|
||||
|
||||
RUNTIME_PYTHONPATH=${PYTHONPATH:-}
|
||||
OFFICIAL_PACKAGE_PARENT="$(dirname -- "${OFFICIAL_PACKAGE_DIR}")"
|
||||
OFFICIAL_PYTHONPATH="${OFFICIAL_PACKAGE_PARENT}${RUNTIME_PYTHONPATH:+:${RUNTIME_PYTHONPATH}}"
|
||||
CURRENT_PYTHONPATH="${CURRENT_OVERLAY_ROOT}${RUNTIME_PYTHONPATH:+:${RUNTIME_PYTHONPATH}}"
|
||||
RAW_RESULTS="${RESULTS_DIR}/performance-runs.jsonl"
|
||||
: > "${RAW_RESULTS}"
|
||||
|
||||
{
|
||||
printf 'captured_at_utc='
|
||||
date -u +%FT%TZ
|
||||
uname -a
|
||||
printf '\n[Python packages]\n'
|
||||
python -V
|
||||
python -m pip show vllm vllm-ascend torch torch-npu triton-ascend
|
||||
printf '\n[NPU]\n'
|
||||
npu-smi info
|
||||
printf '\n[Current source]\n'
|
||||
git -C "${REPO_ROOT}" rev-parse HEAD
|
||||
sha256sum "${REPO_ROOT}"/vllm_ascend/model_executor/offloader/*.py
|
||||
printf '\n[Adapted overlay]\n'
|
||||
sha256sum "${CURRENT_OVERLAY_ROOT}"/vllm_ascend/model_executor/offloader/*.py
|
||||
} > "${RESULTS_DIR}/environment.txt" 2>&1
|
||||
|
||||
run_case() {
|
||||
local implementation=$1
|
||||
local label=$2
|
||||
local group_size=$3
|
||||
local num_in_group=$4
|
||||
local repeat=$5
|
||||
local selected_pythonpath="${CURRENT_PYTHONPATH}"
|
||||
local backend_args=()
|
||||
local log_file="${RESULTS_DIR}/logs/${label}_repeat_${repeat}.log"
|
||||
|
||||
if [[ "${implementation}" == official ]]; then
|
||||
selected_pythonpath="${OFFICIAL_PYTHONPATH}"
|
||||
fi
|
||||
if [[ "${group_size}" -gt 0 ]]; then
|
||||
backend_args=(--offload-backend prefetch)
|
||||
fi
|
||||
|
||||
printf 'implementation=%s case=%s repeat=%s\n' \
|
||||
"${implementation}" "${label}" "${repeat}"
|
||||
env PYTHONPATH="${selected_pythonpath}" \
|
||||
python "${SCRIPT_DIR}/bench_one.py" \
|
||||
--model "${MODEL_PATH}" \
|
||||
--label "${label}" \
|
||||
"${backend_args[@]}" \
|
||||
--offload-group-size "${group_size}" \
|
||||
--offload-num-in-group "${num_in_group}" \
|
||||
--prompt-count "${PROMPT_COUNT}" \
|
||||
--input-tokens "${INPUT_TOKENS}" \
|
||||
--max-tokens "${MAX_TOKENS}" \
|
||||
--max-model-len "${MAX_MODEL_LEN}" \
|
||||
--warmup-rounds "${WARMUP_ROUNDS}" \
|
||||
--measured-rounds "${MEASURED_ROUNDS}" \
|
||||
--seed "${SEED}" \
|
||||
--result-file "${RAW_RESULTS}" \
|
||||
--npu-index "${NPU_INDEX}" \
|
||||
--gpu-memory-utilization "${GPU_MEMORY_UTILIZATION}" \
|
||||
2>&1 | tee "${log_file}"
|
||||
sleep "${COOLDOWN_SECONDS}"
|
||||
}
|
||||
|
||||
# Interleave implementations and positions to reduce time-order bias.
|
||||
RUN_SPECS=(
|
||||
"official:latest_baseline_0_1:0:1:0"
|
||||
"official:latest_official_group_8_num_1:8:1:0"
|
||||
"current:current_impl_group_8_num_1:8:1:0"
|
||||
"official:latest_official_group_8_num_2:8:2:0"
|
||||
"current:current_impl_group_8_num_2:8:2:0"
|
||||
"current:current_impl_group_8_num_2:8:2:1"
|
||||
"official:latest_official_group_8_num_2:8:2:1"
|
||||
"current:current_impl_group_8_num_1:8:1:1"
|
||||
"official:latest_official_group_8_num_1:8:1:1"
|
||||
"official:latest_baseline_0_1:0:1:1"
|
||||
"official:latest_official_group_8_num_1:8:1:2"
|
||||
"current:current_impl_group_8_num_1:8:1:2"
|
||||
"official:latest_baseline_0_1:0:1:2"
|
||||
"current:current_impl_group_8_num_2:8:2:2"
|
||||
"official:latest_official_group_8_num_2:8:2:2"
|
||||
)
|
||||
|
||||
for spec in "${RUN_SPECS[@]}"; do
|
||||
IFS=: read -r implementation label group_size num_in_group repeat <<< "${spec}"
|
||||
run_case \
|
||||
"${implementation}" "${label}" "${group_size}" "${num_in_group}" "${repeat}"
|
||||
done
|
||||
|
||||
python "${SCRIPT_DIR}/bench_param_matrix.py" \
|
||||
--input "${RAW_RESULTS}" \
|
||||
--output-dir "${RESULTS_DIR}" \
|
||||
--baseline-label latest_baseline_0_1
|
||||
|
||||
python - "${RAW_RESULTS}" "${RESULTS_DIR}/logs" "${RESULTS_DIR}/validation.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
runs_path, logs_path, output_path = map(Path, sys.argv[1:])
|
||||
rows = [
|
||||
json.loads(line)
|
||||
for line in runs_path.read_text(encoding="utf-8").splitlines()
|
||||
if line.strip()
|
||||
]
|
||||
expected = {
|
||||
"latest_baseline_0_1": 3,
|
||||
"latest_official_group_8_num_1": 3,
|
||||
"current_impl_group_8_num_1": 3,
|
||||
"latest_official_group_8_num_2": 3,
|
||||
"current_impl_group_8_num_2": 3,
|
||||
}
|
||||
logs = sorted(logs_path.glob("*.log"))
|
||||
counts = Counter(row["label"] for row in rows)
|
||||
current_logs = [path for path in logs if path.name.startswith("current_impl_")]
|
||||
official_logs = [path for path in logs if path.name.startswith("latest_official_")]
|
||||
baseline_logs = [path for path in logs if path.name.startswith("latest_baseline_")]
|
||||
|
||||
validation = {
|
||||
"run_count": len(rows),
|
||||
"log_count": len(logs),
|
||||
"runs_per_label": dict(counts),
|
||||
"current_logs_with_expected_class": sum(
|
||||
"Offloader set to AscendPrefetchOffloader"
|
||||
in path.read_text(encoding="utf-8", errors="replace")
|
||||
for path in current_logs
|
||||
),
|
||||
"official_logs_with_expected_class": sum(
|
||||
"Offloader set to NPUPrefetchOffloader"
|
||||
in path.read_text(encoding="utf-8", errors="replace")
|
||||
for path in official_logs
|
||||
),
|
||||
"baseline_logs_with_prefetch_class": sum(
|
||||
"PrefetchOffloader" in path.read_text(encoding="utf-8", errors="replace")
|
||||
for path in baseline_logs
|
||||
),
|
||||
"logs_with_traceback": [
|
||||
path.name
|
||||
for path in logs
|
||||
if "Traceback (most recent call last)"
|
||||
in path.read_text(encoding="utf-8", errors="replace")
|
||||
],
|
||||
}
|
||||
validation["valid"] = (
|
||||
len(rows) == 15
|
||||
and len(logs) == 15
|
||||
and counts == Counter(expected)
|
||||
and validation["current_logs_with_expected_class"] == 6
|
||||
and validation["official_logs_with_expected_class"] == 6
|
||||
and validation["baseline_logs_with_prefetch_class"] == 0
|
||||
and not validation["logs_with_traceback"]
|
||||
)
|
||||
output_path.write_text(
|
||||
json.dumps(validation, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
if not validation["valid"]:
|
||||
raise SystemExit(json.dumps(validation, indent=2, sort_keys=True))
|
||||
print(json.dumps(validation, indent=2, sort_keys=True))
|
||||
PY
|
||||
|
||||
(
|
||||
cd -- "$(dirname -- "${RESULTS_DIR}")"
|
||||
results_name="$(basename -- "${RESULTS_DIR}")"
|
||||
find "${results_name}" -type f ! -name SHA256SUMS -print0 \
|
||||
| sort -z \
|
||||
| xargs -0 sha256sum > "${results_name}/SHA256SUMS"
|
||||
)
|
||||
|
||||
printf 'Results: %s\n' "${RESULTS_DIR}"
|
||||
|
|
@ -0,0 +1,991 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
RUNNER_TEST_MODE=${RUNNER_TEST_MODE:-0}
|
||||
EXPECTED_CANN_ENV=/data/Ascend/cann/set_env.sh
|
||||
EXPECTED_CANN_ENV_SHA256=787ff0ecb69abe17ed481baecb6e04340c4dc36c655dbedb2718bce13dd289a3
|
||||
EXPECTED_CANN_VERSION_FILE=/etc/Ascend/ascend_cann_install.info
|
||||
EXPECTED_CANN_VERSION_FILE_SHA256=ac255f290ec684bbf360e60afa64740e0f0422ea8e47627c96adda8a922d819e
|
||||
EXPECTED_CANN_PYTHONPATH=/data/Ascend/cann-9.0.1/python/site-packages:/data/Ascend/cann-9.0.1/opp/built-in/op_impl/ai_core/tbe
|
||||
EXPECTED_LATEST_VENV_ACTIVATE=/data/vllm-ascend-v0.23.0rc1/.venv-cann901/bin/activate
|
||||
EXPECTED_LATEST_VENV_ACTIVATE_SHA256=e02077427dc088d48a73206ed4316013ab1d3cf458f255f34b8f9ab2040e75b3
|
||||
EXPECTED_NATIVE_VENV_ACTIVATE=/data/vllm-ascend-v0.18.0/.venv/bin/activate
|
||||
EXPECTED_NATIVE_VENV_ACTIVATE_SHA256=2109fa68160b03b51472db8251938e017d835bd9b660aa1cdb7a5f18e297da19
|
||||
EXPECTED_LATEST_SOURCE_ROOT=/data/vllm-ascend-v0.23.0rc1-source-clean
|
||||
EXPECTED_LATEST_SOURCE_TREE_SHA256=f1677c3dba91ded53030ee2c1538d85efc67c5762c5990814a8dc35b5980194f
|
||||
EXPECTED_NATIVE_SOURCE_ROOT=/data/vllm-ascend-v0.18.0-hardened-v6-source
|
||||
EXPECTED_NATIVE_SOURCE_TREE_SHA256=1c4ae545ce311eb2282f291f1cc315a541754d32eeddc87d82fdb41fdaae6091
|
||||
EXPECTED_LATEST_WHEEL_PATH=/data/vllm-ascend-v0.23.0rc1/vllm_ascend-0.23.0rc1-cp312-cp312-manylinux_2_34_x86_64.whl
|
||||
EXPECTED_LATEST_WHEEL_SHA256=2f02c7a31e6ed565084876aebb6cca0bc8e60d8988fdacb3d0f69f4caa8138c8
|
||||
EXPECTED_LATEST_PACKAGE_TREE_SHA256=58babd7d25b43a81d5f7a4d85f641d388f9b3d16cfe96bed9fb551b660c7ed8f
|
||||
|
||||
if [[ "${RUNNER_TEST_MODE}" == 1 ]]; then
|
||||
case "$(uname -s)" in
|
||||
MINGW*|MSYS*|CYGWIN*) ;;
|
||||
*)
|
||||
echo "RUNNER_TEST_MODE is restricted to the Windows hermetic test harness" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
EXPECTED_CANN_ENV=${RUNNER_TEST_EXPECTED_CANN_ENV:?}
|
||||
EXPECTED_CANN_ENV_SHA256=${RUNNER_TEST_EXPECTED_CANN_ENV_SHA256:?}
|
||||
EXPECTED_CANN_VERSION_FILE=${RUNNER_TEST_EXPECTED_CANN_VERSION_FILE:?}
|
||||
EXPECTED_CANN_VERSION_FILE_SHA256=${RUNNER_TEST_EXPECTED_CANN_VERSION_FILE_SHA256:?}
|
||||
EXPECTED_CANN_PYTHONPATH=${RUNNER_TEST_EXPECTED_CANN_PYTHONPATH:?}
|
||||
EXPECTED_LATEST_VENV_ACTIVATE=${RUNNER_TEST_EXPECTED_LATEST_VENV_ACTIVATE:?}
|
||||
EXPECTED_LATEST_VENV_ACTIVATE_SHA256=${RUNNER_TEST_EXPECTED_LATEST_VENV_ACTIVATE_SHA256:?}
|
||||
EXPECTED_NATIVE_VENV_ACTIVATE=${RUNNER_TEST_EXPECTED_NATIVE_VENV_ACTIVATE:?}
|
||||
EXPECTED_NATIVE_VENV_ACTIVATE_SHA256=${RUNNER_TEST_EXPECTED_NATIVE_VENV_ACTIVATE_SHA256:?}
|
||||
EXPECTED_LATEST_SOURCE_ROOT=${RUNNER_TEST_EXPECTED_LATEST_SOURCE_ROOT:?}
|
||||
EXPECTED_LATEST_SOURCE_TREE_SHA256=${RUNNER_TEST_EXPECTED_LATEST_SOURCE_TREE_SHA256:?}
|
||||
EXPECTED_NATIVE_SOURCE_ROOT=${RUNNER_TEST_EXPECTED_NATIVE_SOURCE_ROOT:?}
|
||||
EXPECTED_NATIVE_SOURCE_TREE_SHA256=${RUNNER_TEST_EXPECTED_NATIVE_SOURCE_TREE_SHA256:?}
|
||||
EXPECTED_LATEST_WHEEL_PATH=${RUNNER_TEST_EXPECTED_LATEST_WHEEL_PATH:?}
|
||||
EXPECTED_LATEST_WHEEL_SHA256=${RUNNER_TEST_EXPECTED_LATEST_WHEEL_SHA256:?}
|
||||
EXPECTED_LATEST_PACKAGE_TREE_SHA256=${RUNNER_TEST_EXPECTED_LATEST_PACKAGE_TREE_SHA256:?}
|
||||
fi
|
||||
|
||||
CANN_ENV=${CANN_ENV:-${EXPECTED_CANN_ENV}}
|
||||
ATB_ENV=${ATB_ENV:-}
|
||||
LATEST_VENV_ACTIVATE=${LATEST_VENV_ACTIVATE:-${EXPECTED_LATEST_VENV_ACTIVATE}}
|
||||
CURRENT_V018_VENV_ACTIVATE=${CURRENT_V018_VENV_ACTIVATE:-${EXPECTED_NATIVE_VENV_ACTIVATE}}
|
||||
LATEST_SOURCE_ROOT=${LATEST_SOURCE_ROOT:-${EXPECTED_LATEST_SOURCE_ROOT}}
|
||||
CURRENT_V018_SOURCE_ROOT=${CURRENT_V018_SOURCE_ROOT:-${EXPECTED_NATIVE_SOURCE_ROOT}}
|
||||
LATEST_WHEEL_PATH=${LATEST_WHEEL_PATH:-${EXPECTED_LATEST_WHEEL_PATH}}
|
||||
MODEL_PATH=${MODEL_PATH:-/data/models/Qwen2.5-7B-Instruct}
|
||||
RESULTS_DIR=${RESULTS_DIR:?RESULTS_DIR must name a new result directory}
|
||||
CANN_VERSION_FILE=${CANN_VERSION_FILE:-${EXPECTED_CANN_VERSION_FILE}}
|
||||
NPU_INDEX=${NPU_INDEX:-$(python3 "${SCRIPT_DIR}/detect_npu_index.py")}
|
||||
NPU_CHIP_INDEX=${NPU_CHIP_INDEX:-0}
|
||||
ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
CASE_TIMEOUT_SECONDS=${CASE_TIMEOUT_SECONDS:-1800}
|
||||
TIMEOUT_KILL_AFTER_SECONDS=${TIMEOUT_KILL_AFTER_SECONDS:-15}
|
||||
|
||||
EXPECTED_MODEL_PATH=/data/models/Qwen2.5-7B-Instruct
|
||||
EXPECTED_MODEL_REVISION=3fe289ea2828843e2d3f4f7449848eae7b1d8e7e
|
||||
EXPECTED_MODEL_SHA256SUMS_SHA256=1feefc067990d64ab8f584793ddbff5bd6cd725cc8a0b91a84d595ebcff04e14
|
||||
EXPECTED_LATEST_COMMIT=f4a08bddd0cc65a0bd8c3d377b158ae5ca7527db
|
||||
EXPECTED_LATEST_COMMIT_SHORT=f4a08bd
|
||||
EXPECTED_NATIVE_COMMIT=72dc68973bd7e6ceef9a122de316a7ee9c9a84aa
|
||||
OVERLAY_ROOT="${RESULTS_DIR}.current_impl_overlay"
|
||||
CACHE_ROOT="${RESULTS_DIR}.cache"
|
||||
|
||||
if [[ "${RUNNER_TEST_MODE}" != 1 && "${MODEL_PATH}" != /data/models/Qwen2.5-7B-Instruct ]]; then
|
||||
echo "MODEL_PATH must be ${EXPECTED_MODEL_PATH}" >&2
|
||||
exit 2
|
||||
fi
|
||||
declare -A FIXED_PATH_IDENTITIES=(
|
||||
[CANN_ENV]="${EXPECTED_CANN_ENV}"
|
||||
[CANN_VERSION_FILE]="${EXPECTED_CANN_VERSION_FILE}"
|
||||
[LATEST_VENV_ACTIVATE]="${EXPECTED_LATEST_VENV_ACTIVATE}"
|
||||
[CURRENT_V018_VENV_ACTIVATE]="${EXPECTED_NATIVE_VENV_ACTIVATE}"
|
||||
[LATEST_SOURCE_ROOT]="${EXPECTED_LATEST_SOURCE_ROOT}"
|
||||
[CURRENT_V018_SOURCE_ROOT]="${EXPECTED_NATIVE_SOURCE_ROOT}"
|
||||
[LATEST_WHEEL_PATH]="${EXPECTED_LATEST_WHEEL_PATH}"
|
||||
)
|
||||
for identity_name in "${!FIXED_PATH_IDENTITIES[@]}"; do
|
||||
if [[ "${!identity_name}" != "${FIXED_PATH_IDENTITIES[${identity_name}]}" ]]; then
|
||||
echo "${identity_name} must be ${FIXED_PATH_IDENTITIES[${identity_name}]}" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
if [[ -n "${ATB_ENV}" ]]; then
|
||||
echo "ATB_ENV must remain empty for the pinned comparison runtime" >&2
|
||||
exit 2
|
||||
fi
|
||||
[[ -f "${CANN_ENV}" ]]
|
||||
[[ -f "${CANN_VERSION_FILE}" ]]
|
||||
[[ -f "${LATEST_VENV_ACTIVATE}" ]]
|
||||
[[ -f "${CURRENT_V018_VENV_ACTIVATE}" ]]
|
||||
[[ -f "${LATEST_WHEEL_PATH}" ]]
|
||||
[[ -d "${LATEST_SOURCE_ROOT}" ]]
|
||||
[[ -d "${CURRENT_V018_SOURCE_ROOT}" ]]
|
||||
[[ -d "${MODEL_PATH}" ]]
|
||||
[[ -f "${MODEL_PATH}/.msc" ]]
|
||||
[[ -f "${MODEL_PATH}/SHA256SUMS" ]]
|
||||
|
||||
for owned_path in "${RESULTS_DIR}" "${OVERLAY_ROOT}" "${CACHE_ROOT}"; do
|
||||
if [[ -e "${owned_path}" || -L "${owned_path}" ]]; then
|
||||
echo "refusing to reuse existing run-owned path ${owned_path}" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
mkdir -- "${RESULTS_DIR}"
|
||||
mkdir -- "${OVERLAY_ROOT}"
|
||||
mkdir -- "${CACHE_ROOT}"
|
||||
mkdir -p "${RESULTS_DIR}/tmp"
|
||||
|
||||
CANN_ENV_SHA256="$(sha256sum "${CANN_ENV}" | awk '{print $1}')"
|
||||
if [[ "${CANN_ENV_SHA256}" != "${EXPECTED_CANN_ENV_SHA256}" ]]; then
|
||||
echo "CANN activation script identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
CANN_VERSION_FILE_SHA256="$(sha256sum "${CANN_VERSION_FILE}" | awk '{print $1}')"
|
||||
if [[ "${CANN_VERSION_FILE_SHA256}" != "${EXPECTED_CANN_VERSION_FILE_SHA256}" ]]; then
|
||||
echo "CANN version authority identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
LATEST_VENV_ACTIVATE_SHA256="$(
|
||||
sha256sum "${LATEST_VENV_ACTIVATE}" | awk '{print $1}'
|
||||
)"
|
||||
if [[ "${LATEST_VENV_ACTIVATE_SHA256}" != "${EXPECTED_LATEST_VENV_ACTIVATE_SHA256}" ]]; then
|
||||
echo "latest venv activation script identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
NATIVE_VENV_ACTIVATE_SHA256="$(
|
||||
sha256sum "${CURRENT_V018_VENV_ACTIVATE}" | awk '{print $1}'
|
||||
)"
|
||||
if [[ "${NATIVE_VENV_ACTIVATE_SHA256}" != "${EXPECTED_NATIVE_VENV_ACTIVATE_SHA256}" ]]; then
|
||||
echo "native v0.18 venv activation script identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
export PYTHONPATH=
|
||||
source "${CANN_ENV}"
|
||||
CANN_PYTHONPATH=
|
||||
IFS=: read -r -a cann_python_paths <<< "${PYTHONPATH:-}"
|
||||
for cann_python_path in "${cann_python_paths[@]}"; do
|
||||
[[ -n "${cann_python_path}" ]] || continue
|
||||
CANN_PYTHONPATH="${CANN_PYTHONPATH:+${CANN_PYTHONPATH}:}${cann_python_path}"
|
||||
done
|
||||
if [[ -z "${CANN_PYTHONPATH}" ]]; then
|
||||
echo "CANN environment did not provide trusted Python paths" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "${CANN_PYTHONPATH}" != "${EXPECTED_CANN_PYTHONPATH}" ]]; then
|
||||
echo "CANN Python path identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
export CANN_PYTHONPATH
|
||||
export PYTHONPATH="${CANN_PYTHONPATH}"
|
||||
if [[ -n "${ATB_ENV}" ]]; then
|
||||
[[ -f "${ATB_ENV}" ]]
|
||||
source "${ATB_ENV}"
|
||||
fi
|
||||
source "${LATEST_VENV_ACTIVATE}"
|
||||
LATEST_PYTHON="$(command -v python)"
|
||||
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export VLLM_USE_MODELSCOPE=True
|
||||
export VLLM_NO_USAGE_STATS=1
|
||||
export ASCEND_RT_VISIBLE_DEVICES NPU_INDEX NPU_CHIP_INDEX
|
||||
|
||||
bind_run_cache() {
|
||||
export XDG_CACHE_HOME="${CACHE_ROOT}/xdg"
|
||||
export VLLM_CACHE_ROOT="${CACHE_ROOT}/vllm"
|
||||
export HF_HOME="${CACHE_ROOT}/huggingface"
|
||||
export TRITON_CACHE_DIR="${CACHE_ROOT}/triton"
|
||||
mkdir -p \
|
||||
"${XDG_CACHE_HOME}" "${VLLM_CACHE_ROOT}" \
|
||||
"${HF_HOME}" "${TRITON_CACHE_DIR}"
|
||||
}
|
||||
bind_run_cache
|
||||
|
||||
SAME_RUNTIME_CASES=(
|
||||
"baseline:official:0:1"
|
||||
"current_28_1:current:28:1" "latest_28_1:official:28:1"
|
||||
"current_14_1:current:14:1" "latest_14_1:official:14:1"
|
||||
"current_28_2:current:28:2" "latest_28_2:official:28:2"
|
||||
"current_8_1:current:8:1" "latest_8_1:official:8:1"
|
||||
"current_7_1:current:7:1" "latest_7_1:official:7:1"
|
||||
"current_14_2:current:14:2" "latest_14_2:official:14:2"
|
||||
"current_8_2:current:8:2" "latest_8_2:official:8:2"
|
||||
"current_4_1:current:4:1" "latest_4_1:official:4:1"
|
||||
"current_2_1:current:2:1" "latest_2_1:official:2:1"
|
||||
"current_4_2:current:4:2" "latest_4_2:official:4:2"
|
||||
)
|
||||
LONG_WINDOW_CASES=(
|
||||
"baseline:official:0:1"
|
||||
"current_28_1:current:28:1" "latest_28_1:official:28:1"
|
||||
"current_14_1:current:14:1" "latest_14_1:official:14:1"
|
||||
)
|
||||
NATIVE_V018_CASES=(
|
||||
"baseline:current:0:1"
|
||||
"current_28_1:current:28:1"
|
||||
"current_14_1:current:14:1"
|
||||
"current_8_1:current:8:1"
|
||||
)
|
||||
|
||||
hash_tree() {
|
||||
local tree=$1
|
||||
(
|
||||
cd "${tree}"
|
||||
find . -type f \
|
||||
! -path '*/__pycache__/*' \
|
||||
! -path '*/.pytest_cache/*' \
|
||||
! -path '*/.mypy_cache/*' \
|
||||
! -path '*/.ruff_cache/*' \
|
||||
! -name '*.pyc' \
|
||||
! -name '*.pyo' \
|
||||
-print0 | sort -z | xargs -0 sha256sum |
|
||||
sed -E 's/^([0-9a-f]{64}) [* ]/\1 /'
|
||||
) | sha256sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
write_tree_manifest() {
|
||||
local tree=$1
|
||||
local output=$2
|
||||
(
|
||||
cd "${tree}"
|
||||
find . -type f \
|
||||
! -path '*/__pycache__/*' \
|
||||
! -path '*/.pytest_cache/*' \
|
||||
! -path '*/.mypy_cache/*' \
|
||||
! -path '*/.ruff_cache/*' \
|
||||
! -name '*.pyc' \
|
||||
! -name '*.pyo' \
|
||||
-print0 | sort -z | xargs -0 sha256sum |
|
||||
sed -E 's/^([0-9a-f]{64}) [* ]/\1 /'
|
||||
) > "${output}"
|
||||
}
|
||||
|
||||
write_wheel_package_manifest() {
|
||||
local wheel=$1
|
||||
local output=$2
|
||||
"${LATEST_PYTHON}" - "${wheel}" "${output}" <<'PY'
|
||||
import hashlib
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
wheel_package_tree_manifest = True
|
||||
wheel_path, output_path = map(Path, sys.argv[1:])
|
||||
rows = []
|
||||
with zipfile.ZipFile(wheel_path) as archive:
|
||||
for name in sorted(archive.namelist()):
|
||||
if not name.startswith("vllm_ascend/") or name.endswith("/"):
|
||||
continue
|
||||
relative = name.removeprefix("vllm_ascend/")
|
||||
parts = relative.split("/")
|
||||
if (
|
||||
"__pycache__" in parts
|
||||
or any(
|
||||
part in {".pytest_cache", ".mypy_cache", ".ruff_cache"}
|
||||
for part in parts
|
||||
)
|
||||
or relative.endswith((".pyc", ".pyo"))
|
||||
):
|
||||
continue
|
||||
digest = hashlib.sha256(archive.read(name)).hexdigest()
|
||||
rows.append(f"{digest} ./{relative}\n")
|
||||
output_path.write_text("".join(rows), encoding="utf-8", newline="\n")
|
||||
PY
|
||||
}
|
||||
|
||||
capture_package_versions() {
|
||||
local interpreter=$1
|
||||
local runtime_flavor=$2
|
||||
EXPECTED_RUNTIME_FLAVOR="${runtime_flavor}" "${interpreter}" - <<'PY'
|
||||
import importlib.metadata
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
EXPECTED_PACKAGE_VERSIONS = {
|
||||
"latest": {
|
||||
"vllm": "0.23.0",
|
||||
"vllm-ascend": "0.23.0rc1",
|
||||
"torch": "2.10.0+cpu",
|
||||
"torch-npu": "2.10.0.post2",
|
||||
"triton-ascend": "3.2.1",
|
||||
},
|
||||
"native_v018": {
|
||||
"vllm": "0.18.0+empty",
|
||||
"vllm-ascend": "0.1.dev1+g72dc68973.d20260729",
|
||||
"torch": "2.9.0+cpu",
|
||||
"torch-npu": "2.9.0.post2",
|
||||
"triton-ascend": "3.2.1",
|
||||
},
|
||||
}
|
||||
runtime_flavor = os.environ["EXPECTED_RUNTIME_FLAVOR"]
|
||||
expected = EXPECTED_PACKAGE_VERSIONS[runtime_flavor]
|
||||
actual = {name: importlib.metadata.version(name) for name in expected}
|
||||
if actual != expected:
|
||||
raise SystemExit(
|
||||
f"{runtime_flavor} package identity mismatch: "
|
||||
f"expected {expected!r}, found {actual!r}"
|
||||
)
|
||||
print(json.dumps(actual, sort_keys=True, separators=(",", ":")))
|
||||
PY
|
||||
}
|
||||
|
||||
LATEST_PACKAGE_JSON="$(capture_package_versions "${LATEST_PYTHON}" latest)"
|
||||
NATIVE_PACKAGE_JSON="$(
|
||||
(
|
||||
source "${CURRENT_V018_VENV_ACTIVATE}"
|
||||
bind_run_cache
|
||||
capture_package_versions "$(command -v python)" native_v018
|
||||
)
|
||||
)"
|
||||
|
||||
LATEST_SOURCE_COMMIT="$(git -C "${LATEST_SOURCE_ROOT}" rev-parse HEAD)"
|
||||
LATEST_SOURCE_COMMIT_SHORT="$(git -C "${LATEST_SOURCE_ROOT}" rev-parse --short=7 HEAD)"
|
||||
NATIVE_SOURCE_COMMIT="$(git -C "${CURRENT_V018_SOURCE_ROOT}" rev-parse HEAD)"
|
||||
if [[
|
||||
"${LATEST_SOURCE_COMMIT}" != "${EXPECTED_LATEST_COMMIT}" ||
|
||||
"${LATEST_SOURCE_COMMIT_SHORT}" != "${EXPECTED_LATEST_COMMIT_SHORT}"
|
||||
]]; then
|
||||
echo "latest source commit must be ${EXPECTED_LATEST_COMMIT}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "${NATIVE_SOURCE_COMMIT}" != "${EXPECTED_NATIVE_COMMIT}" ]]; then
|
||||
echo "native v0.18 source commit must be ${EXPECTED_NATIVE_COMMIT}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
OFFICIAL_PACKAGE_DIR="$("${LATEST_PYTHON}" - <<'PY'
|
||||
import importlib.metadata
|
||||
print(importlib.metadata.distribution("vllm-ascend").locate_file("vllm_ascend"))
|
||||
PY
|
||||
)"
|
||||
OFFICIAL_PACKAGE_PARENT="$(dirname "${OFFICIAL_PACKAGE_DIR}")"
|
||||
write_tree_manifest \
|
||||
"${OFFICIAL_PACKAGE_DIR}" \
|
||||
"${RESULTS_DIR}/latest-installed-package-sha256s.txt"
|
||||
LATEST_PACKAGE_TREE_SHA256="$(
|
||||
sha256sum "${RESULTS_DIR}/latest-installed-package-sha256s.txt" |
|
||||
awk '{print $1}'
|
||||
)"
|
||||
if [[ "${LATEST_PACKAGE_TREE_SHA256}" != "${EXPECTED_LATEST_PACKAGE_TREE_SHA256}" ]]; then
|
||||
echo "latest installed package tree identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
LATEST_WHEEL_SHA256="$(sha256sum "${LATEST_WHEEL_PATH}" | awk '{print $1}')"
|
||||
if [[ "${LATEST_WHEEL_SHA256}" != "${EXPECTED_LATEST_WHEEL_SHA256}" ]]; then
|
||||
echo "latest wheel identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
write_wheel_package_manifest \
|
||||
"${LATEST_WHEEL_PATH}" \
|
||||
"${RESULTS_DIR}/latest-wheel-package-sha256s.txt"
|
||||
LATEST_WHEEL_PACKAGE_TREE_SHA256="$(
|
||||
sha256sum "${RESULTS_DIR}/latest-wheel-package-sha256s.txt" |
|
||||
awk '{print $1}'
|
||||
)"
|
||||
if [[
|
||||
"${LATEST_WHEEL_PACKAGE_TREE_SHA256}" != "${EXPECTED_LATEST_PACKAGE_TREE_SHA256}" ||
|
||||
"${LATEST_WHEEL_PACKAGE_TREE_SHA256}" != "${LATEST_PACKAGE_TREE_SHA256}"
|
||||
]]; then
|
||||
echo "latest wheel package tree does not match the installed package" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
write_tree_manifest \
|
||||
"${LATEST_SOURCE_ROOT}/vllm_ascend" \
|
||||
"${RESULTS_DIR}/latest-source-tree-sha256s.txt"
|
||||
LATEST_SOURCE_TREE_SHA256="$(
|
||||
sha256sum "${RESULTS_DIR}/latest-source-tree-sha256s.txt" |
|
||||
awk '{print $1}'
|
||||
)"
|
||||
if [[ "${LATEST_SOURCE_TREE_SHA256}" != "${EXPECTED_LATEST_SOURCE_TREE_SHA256}" ]]; then
|
||||
echo "latest source checkout tree identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
write_tree_manifest \
|
||||
"${CURRENT_V018_SOURCE_ROOT}/vllm_ascend" \
|
||||
"${RESULTS_DIR}/native-v018-source-tree-sha256s.txt"
|
||||
NATIVE_SOURCE_TREE_SHA256="$(
|
||||
sha256sum "${RESULTS_DIR}/native-v018-source-tree-sha256s.txt" |
|
||||
awk '{print $1}'
|
||||
)"
|
||||
if [[ "${NATIVE_SOURCE_TREE_SHA256}" != "${EXPECTED_NATIVE_SOURCE_TREE_SHA256}" ]]; then
|
||||
echo "native v0.18 source tree identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
CANN_VERSION="$(
|
||||
grep -Eo '(^|[^0-9])9\.0\.1([^0-9]|$)' "${CANN_VERSION_FILE}" |
|
||||
sed -E 's/[^0-9]*(9\.0\.1)[^0-9]*/\1/' | sort -u
|
||||
)"
|
||||
if [[ "${CANN_VERSION}" != 9.0.1 ]]; then
|
||||
echo "CANN version file must identify exactly version 9.0.1" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
MODEL_REVISION="$("${LATEST_PYTHON}" - "${MODEL_PATH}/.msc" <<'PY'
|
||||
import pickle
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
with Path(sys.argv[1]).open("rb") as handle:
|
||||
metadata = pickle.load(handle)
|
||||
if not isinstance(metadata, list):
|
||||
raise SystemExit("model .msc must contain a list")
|
||||
revisions = [
|
||||
item.get("Revision")
|
||||
for item in metadata
|
||||
if isinstance(item, dict) and item.get("Path") == "config.json"
|
||||
]
|
||||
if len(revisions) != 1 or not isinstance(revisions[0], str):
|
||||
raise SystemExit("model .msc must contain one config.json Revision")
|
||||
print(revisions[0])
|
||||
PY
|
||||
)"
|
||||
if [[ "${MODEL_REVISION}" != "${EXPECTED_MODEL_REVISION}" ]]; then
|
||||
echo "model .msc revision mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
MODEL_SHA256SUMS_SHA256="$(
|
||||
sha256sum "${MODEL_PATH}/SHA256SUMS" | awk '{print $1}'
|
||||
)"
|
||||
if [[ "${RUNNER_TEST_MODE:-0}" == 1 ]]; then
|
||||
EXPECTED_MODEL_SHA256SUMS_SHA256="${MODEL_SHA256SUMS_SHA256}"
|
||||
fi
|
||||
if [[ "${MODEL_SHA256SUMS_SHA256}" != "${EXPECTED_MODEL_SHA256SUMS_SHA256}" ]]; then
|
||||
echo "model SHA256SUMS identity mismatch" >&2
|
||||
exit 2
|
||||
fi
|
||||
(
|
||||
cd "${MODEL_PATH}"
|
||||
sha256sum -c SHA256SUMS
|
||||
) > "${RESULTS_DIR}/model-manifest-check.log"
|
||||
|
||||
PROJECT_OFFLOADER_HASH="$(hash_tree "${REPO_ROOT}/vllm_ascend/model_executor/offloader")"
|
||||
PROJECT_WORKER_HASH="$(
|
||||
sha256sum "${REPO_ROOT}/vllm_ascend/worker/model_runner_v1.py" |
|
||||
awk '{print $1}'
|
||||
)"
|
||||
PROJECT_RUNNER_HASH="$(sha256sum "${SCRIPT_DIR}/run_large_model_parameter_comparison.sh" | awk '{print $1}')"
|
||||
|
||||
export MODEL_PATH RESULTS_DIR LATEST_SOURCE_ROOT CURRENT_V018_SOURCE_ROOT
|
||||
export PROJECT_OFFLOADER_HASH PROJECT_WORKER_HASH PROJECT_RUNNER_HASH
|
||||
export CANN_ENV CANN_ENV_SHA256 CANN_VERSION CANN_VERSION_FILE
|
||||
export CANN_VERSION_FILE_SHA256
|
||||
export CANN_PYTHONPATH ATB_ENV
|
||||
export LATEST_VENV_ACTIVATE CURRENT_V018_VENV_ACTIVATE
|
||||
export LATEST_VENV_ACTIVATE_SHA256 NATIVE_VENV_ACTIVATE_SHA256
|
||||
export LATEST_PACKAGE_JSON NATIVE_PACKAGE_JSON
|
||||
export LATEST_SOURCE_COMMIT LATEST_SOURCE_COMMIT_SHORT NATIVE_SOURCE_COMMIT
|
||||
export LATEST_SOURCE_TREE_SHA256 NATIVE_SOURCE_TREE_SHA256
|
||||
export LATEST_WHEEL_PATH LATEST_WHEEL_SHA256
|
||||
export LATEST_WHEEL_PACKAGE_TREE_SHA256 LATEST_PACKAGE_TREE_SHA256
|
||||
export OFFICIAL_PACKAGE_DIR OFFICIAL_PACKAGE_PARENT
|
||||
export MODEL_REVISION MODEL_SHA256SUMS_SHA256 CACHE_ROOT OVERLAY_ROOT
|
||||
|
||||
"${LATEST_PYTHON}" - "${SCRIPT_DIR}/analyze_large_model_parameter_comparison.py" \
|
||||
> "${RESULTS_DIR}/environment-manifest.json.tmp" <<'PY'
|
||||
import importlib.metadata
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
environment_manifest_payload = True
|
||||
analyzer_path = Path(sys.argv[1])
|
||||
spec = importlib.util.spec_from_file_location("qwen_comparison_analyzer", analyzer_path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise SystemExit(f"could not load {analyzer_path}")
|
||||
analyzer = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(analyzer)
|
||||
layer_weight_bytes = analyzer.read_layer_weight_bytes(
|
||||
Path(os.environ["MODEL_PATH"]), 28,
|
||||
)
|
||||
payload = {
|
||||
"schema_version": 3,
|
||||
"results_dir": os.environ["RESULTS_DIR"],
|
||||
"npu_index": int(os.environ["NPU_INDEX"]),
|
||||
"ascend_rt_visible_devices": os.environ["ASCEND_RT_VISIBLE_DEVICES"],
|
||||
"cache": {
|
||||
"root": os.environ["CACHE_ROOT"],
|
||||
"xdg": os.environ["XDG_CACHE_HOME"],
|
||||
"vllm": os.environ["VLLM_CACHE_ROOT"],
|
||||
"huggingface": os.environ["HF_HOME"],
|
||||
"triton": os.environ["TRITON_CACHE_DIR"],
|
||||
},
|
||||
"cann": {
|
||||
"activation_script_sha256": os.environ["CANN_ENV_SHA256"],
|
||||
"version": os.environ["CANN_VERSION"],
|
||||
"version_file": os.environ["CANN_VERSION_FILE"],
|
||||
"version_file_sha256": os.environ["CANN_VERSION_FILE_SHA256"],
|
||||
"python_paths": os.environ["CANN_PYTHONPATH"].split(":"),
|
||||
},
|
||||
"latest_runtime": {
|
||||
"installed_package_root": os.environ["OFFICIAL_PACKAGE_DIR"],
|
||||
"installed_package_tree_sha256": os.environ["LATEST_PACKAGE_TREE_SHA256"],
|
||||
"package_versions": json.loads(os.environ["LATEST_PACKAGE_JSON"]),
|
||||
"source_root": os.environ["LATEST_SOURCE_ROOT"],
|
||||
"source_commit": os.environ["LATEST_SOURCE_COMMIT"],
|
||||
"source_commit_short": os.environ["LATEST_SOURCE_COMMIT_SHORT"],
|
||||
"source_tree_sha256": os.environ["LATEST_SOURCE_TREE_SHA256"],
|
||||
"venv_activate_sha256": os.environ["LATEST_VENV_ACTIVATE_SHA256"],
|
||||
"wheel_package_tree_sha256": os.environ[
|
||||
"LATEST_WHEEL_PACKAGE_TREE_SHA256"
|
||||
],
|
||||
"wheel_path": os.environ["LATEST_WHEEL_PATH"],
|
||||
"wheel_sha256": os.environ["LATEST_WHEEL_SHA256"],
|
||||
},
|
||||
"native_v018_runtime": {
|
||||
"package_versions": json.loads(os.environ["NATIVE_PACKAGE_JSON"]),
|
||||
"source_root": os.environ["CURRENT_V018_SOURCE_ROOT"],
|
||||
"source_commit": os.environ["NATIVE_SOURCE_COMMIT"],
|
||||
"source_tree_sha256": os.environ["NATIVE_SOURCE_TREE_SHA256"],
|
||||
"venv_activate_sha256": os.environ["NATIVE_VENV_ACTIVATE_SHA256"],
|
||||
},
|
||||
"model": {
|
||||
"id": "Qwen/Qwen2.5-7B-Instruct",
|
||||
"path": os.environ["MODEL_PATH"],
|
||||
"layer_count": 28,
|
||||
"modelscope_revision": os.environ["MODEL_REVISION"],
|
||||
"sha256s_file": str(Path(os.environ["MODEL_PATH"]) / "SHA256SUMS"),
|
||||
"sha256s_sha256": os.environ["MODEL_SHA256SUMS_SHA256"],
|
||||
"layer_weight_bytes": layer_weight_bytes,
|
||||
},
|
||||
"project_source": {
|
||||
"offloader_tree_sha256": os.environ["PROJECT_OFFLOADER_HASH"],
|
||||
"worker_model_runner_sha256": os.environ["PROJECT_WORKER_HASH"],
|
||||
"runner_sha256": os.environ["PROJECT_RUNNER_HASH"],
|
||||
},
|
||||
"environment": {
|
||||
name: os.environ[name]
|
||||
for name in (
|
||||
"CANN_ENV", "ATB_ENV", "LATEST_VENV_ACTIVATE",
|
||||
"CURRENT_V018_VENV_ACTIVATE", "CANN_PYTHONPATH", "XDG_CACHE_HOME",
|
||||
"VLLM_CACHE_ROOT", "HF_HOME", "TRITON_CACHE_DIR",
|
||||
"VLLM_NO_USAGE_STATS",
|
||||
)
|
||||
},
|
||||
}
|
||||
payload["latest_vllm_ascend_distribution"] = os.environ[
|
||||
"OFFICIAL_PACKAGE_PARENT"
|
||||
]
|
||||
print(json.dumps(payload, indent=2, sort_keys=True))
|
||||
PY
|
||||
mv "${RESULTS_DIR}/environment-manifest.json.tmp" \
|
||||
"${RESULTS_DIR}/environment-manifest.json"
|
||||
ENVIRONMENT_MANIFEST_SHA256="$(
|
||||
sha256sum "${RESULTS_DIR}/environment-manifest.json" | awk '{print $1}'
|
||||
)"
|
||||
export ENVIRONMENT_MANIFEST_SHA256
|
||||
|
||||
npu-smi info > "${RESULTS_DIR}/npu-state-before.log" 2>&1
|
||||
df -h "${MODEL_PATH}" "${RESULTS_DIR}" > "${RESULTS_DIR}/disk-space-before.log"
|
||||
find "${REPO_ROOT}/vllm_ascend/model_executor/offloader" -type f \
|
||||
! -path '*/__pycache__/*' ! -name '*.pyc' ! -name '*.pyo' -print0 | sort -z | xargs -0 sha256sum \
|
||||
> "${RESULTS_DIR}/project-offloader-sha256s.txt"
|
||||
sha256sum "${REPO_ROOT}/vllm_ascend/worker/model_runner_v1.py" \
|
||||
> "${RESULTS_DIR}/project-worker-sha256.txt"
|
||||
sha256sum "${SCRIPT_DIR}/run_large_model_parameter_comparison.sh" \
|
||||
> "${RESULTS_DIR}/project-runner-sha256.txt"
|
||||
cp "${MODEL_PATH}/SHA256SUMS" "${RESULTS_DIR}/model-sha256s.txt"
|
||||
"${LATEST_PYTHON}" -m pip freeze > "${RESULTS_DIR}/latest-package-versions.txt"
|
||||
|
||||
OVERLAY_PACKAGE_DIR="${OVERLAY_ROOT}/vllm_ascend"
|
||||
cp -a "${OFFICIAL_PACKAGE_DIR}" "${OVERLAY_PACKAGE_DIR}"
|
||||
rm -rf -- "${OVERLAY_PACKAGE_DIR}/model_executor/offloader"
|
||||
cp -a "${REPO_ROOT}/vllm_ascend/model_executor/offloader" \
|
||||
"${OVERLAY_PACKAGE_DIR}/model_executor/offloader"
|
||||
OVERLAY_PREFETCH="${OVERLAY_PACKAGE_DIR}/model_executor/offloader/prefetch.py"
|
||||
if ! grep -Fqx "NPUPrefetchOffloader = AscendPrefetchOffloader" "${OVERLAY_PREFETCH}"; then
|
||||
printf '\nNPUPrefetchOffloader = AscendPrefetchOffloader\n' >> "${OVERLAY_PREFETCH}"
|
||||
fi
|
||||
write_tree_manifest \
|
||||
"${OVERLAY_PACKAGE_DIR}" \
|
||||
"${RESULTS_DIR}/current-overlay-package-sha256s.txt"
|
||||
OVERLAY_PACKAGE_TREE_SHA256="$(
|
||||
sha256sum "${RESULTS_DIR}/current-overlay-package-sha256s.txt" |
|
||||
awk '{print $1}'
|
||||
)"
|
||||
EVIDENCE_HOOK_ROOT="${RESULTS_DIR}/evidence-hook"
|
||||
cp -a "${SCRIPT_DIR}/qwen_offload_evidence_hook" "${EVIDENCE_HOOK_ROOT}"
|
||||
|
||||
verify_latest_runtime_identity() {
|
||||
if [[ "$(hash_tree "${OFFICIAL_PACKAGE_DIR}")" != "${LATEST_PACKAGE_TREE_SHA256}" ]]; then
|
||||
echo "latest installed package tree changed during the run" >&2
|
||||
exit 3
|
||||
fi
|
||||
if [[ "$(hash_tree "${LATEST_SOURCE_ROOT}/vllm_ascend")" != "${LATEST_SOURCE_TREE_SHA256}" ]]; then
|
||||
echo "latest source checkout tree changed during the run" >&2
|
||||
exit 3
|
||||
fi
|
||||
if [[ "$(sha256sum "${LATEST_WHEEL_PATH}" | awk '{print $1}')" != "${LATEST_WHEEL_SHA256}" ]]; then
|
||||
echo "latest wheel changed during the run" >&2
|
||||
exit 3
|
||||
fi
|
||||
if [[ "$(hash_tree "${OVERLAY_PACKAGE_DIR}")" != "${OVERLAY_PACKAGE_TREE_SHA256}" ]]; then
|
||||
echo "current implementation overlay changed during the run" >&2
|
||||
exit 3
|
||||
fi
|
||||
if [[ "$(sha256sum "${CANN_ENV}" | awk '{print $1}')" != "${CANN_ENV_SHA256}" ]]; then
|
||||
echo "CANN activation script changed during the run" >&2
|
||||
exit 3
|
||||
fi
|
||||
if [[ "$(sha256sum "${CANN_VERSION_FILE}" | awk '{print $1}')" != "${CANN_VERSION_FILE_SHA256}" ]]; then
|
||||
echo "CANN version authority changed during the run" >&2
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
run_health_before_phase() {
|
||||
local phase=$1
|
||||
local phase_health="${RESULTS_DIR}/${phase}-health-before.log"
|
||||
bash "${SCRIPT_DIR}/check_npu_health.sh" > "${phase_health}" 2>&1
|
||||
if [[ "${phase}" == same-runtime ]]; then
|
||||
cp "${phase_health}" "${RESULTS_DIR}/npu-health-before.log"
|
||||
fi
|
||||
}
|
||||
|
||||
finalize_case() {
|
||||
local raw_result=$1
|
||||
local raw_log=$2
|
||||
local raw_evidence=$3
|
||||
local phase_result=$4
|
||||
local final_log=$5
|
||||
local enriched_result="${raw_result}.enriched"
|
||||
local enriched_log="${raw_log}.enriched"
|
||||
|
||||
python - "${raw_result}" "${raw_log}" "${raw_evidence}" \
|
||||
"${enriched_result}" "${enriched_log}" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
raw_result, raw_log, raw_evidence, enriched_result, enriched_log = map(
|
||||
Path, sys.argv[1:]
|
||||
)
|
||||
records = [json.loads(line) for line in raw_result.read_text(encoding="utf-8").splitlines() if line]
|
||||
if len(records) != 1 or not isinstance(records[0], dict):
|
||||
raise SystemExit("benchmark did not produce exactly one JSONL record")
|
||||
lines = raw_log.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
result_lines = [line for line in lines if line.startswith("BENCH_RESULT ")]
|
||||
if len(result_lines) != 1:
|
||||
raise SystemExit("benchmark log did not contain exactly one BENCH_RESULT line")
|
||||
try:
|
||||
logged_record = json.loads(result_lines[0].removeprefix("BENCH_RESULT "))
|
||||
except json.JSONDecodeError as exc:
|
||||
raise SystemExit("benchmark log has invalid BENCH_RESULT JSON") from exc
|
||||
if not isinstance(logged_record, dict) or logged_record != records[0]:
|
||||
raise SystemExit("BENCH_RESULT JSON does not match raw JSONL record")
|
||||
try:
|
||||
evidence = json.loads(raw_evidence.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError) as exc:
|
||||
raise SystemExit("runtime offload evidence is missing or invalid") from exc
|
||||
if not isinstance(evidence, dict):
|
||||
raise SystemExit("runtime offload evidence must be one JSON object")
|
||||
evidence_name = "OFFLOAD_RUNTIME_EVIDENCE"
|
||||
evidence_marker = evidence_name + " "
|
||||
evidence_lines = []
|
||||
for line in lines:
|
||||
if evidence_name not in line:
|
||||
continue
|
||||
if line.count(evidence_marker) != 1:
|
||||
raise SystemExit(
|
||||
"benchmark log has malformed runtime offload evidence marker"
|
||||
)
|
||||
prefix, payload = line.split(evidence_marker, 1)
|
||||
if evidence_name in prefix or evidence_name in payload or not payload:
|
||||
raise SystemExit(
|
||||
"benchmark log has malformed runtime offload evidence marker"
|
||||
)
|
||||
evidence_lines.append(payload)
|
||||
if len(evidence_lines) != 1:
|
||||
raise SystemExit("benchmark log must contain one runtime offload evidence line")
|
||||
try:
|
||||
logged_evidence = json.loads(evidence_lines[0])
|
||||
except json.JSONDecodeError as exc:
|
||||
raise SystemExit("benchmark log has invalid runtime offload evidence") from exc
|
||||
if logged_evidence != evidence:
|
||||
raise SystemExit("runtime offload evidence file and log disagree")
|
||||
required_evidence = {
|
||||
"schema_version", "hook_loaded", "offloader_class", "group_size",
|
||||
"num_in_group", "selected_layers", "wrapped_count", "offloaded_bytes",
|
||||
}
|
||||
if set(evidence) != required_evidence or evidence["schema_version"] != 1:
|
||||
raise SystemExit("runtime offload evidence schema mismatch")
|
||||
if evidence["hook_loaded"] is not True:
|
||||
raise SystemExit("runtime offload evidence hook was not loaded")
|
||||
matches = []
|
||||
for line in lines:
|
||||
match = re.search(r"Available KV cache memory:\s*([0-9]+(?:\.[0-9]+)?)\s*GiB", line)
|
||||
if match:
|
||||
matches.append(match.group(1))
|
||||
if len(matches) != 1:
|
||||
raise SystemExit("could not capture available KV cache memory")
|
||||
record = records[0]
|
||||
record["available_kv_cache_gib"] = float(matches[0])
|
||||
record["actual_offload_evidence"] = evidence
|
||||
record["environment_manifest_sha256"] = os.environ[
|
||||
"ENVIRONMENT_MANIFEST_SHA256"
|
||||
]
|
||||
record["runtime_identity"] = os.environ["RUNTIME_IDENTITY"]
|
||||
record["selected_layers"] = evidence["selected_layers"]
|
||||
record["selected_layer_count"] = evidence["wrapped_count"]
|
||||
record["selected_weight_bytes"] = evidence["offloaded_bytes"]
|
||||
fingerprint = json.dumps(record, sort_keys=True, separators=(",", ":"))
|
||||
Path(enriched_result).write_text(fingerprint + "\n", encoding="utf-8")
|
||||
diagnostics = [line for line in lines if not line.startswith("BENCH_RESULT ")]
|
||||
Path(enriched_log).write_text(
|
||||
"\n".join([*diagnostics, "BENCH_RESULT " + fingerprint]) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
PY
|
||||
|
||||
python - "${enriched_result}" "${phase_result}" <<'PY'
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
record, destination = map(Path, sys.argv[1:])
|
||||
payload = record.read_bytes()
|
||||
fd = os.open(destination, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
|
||||
try:
|
||||
os.write(fd, payload)
|
||||
os.fsync(fd)
|
||||
finally:
|
||||
os.close(fd)
|
||||
PY
|
||||
mv "${enriched_log}" "${final_log}"
|
||||
rm -f -- "${raw_result}" "${raw_evidence}" "${enriched_result}" "${raw_log}"
|
||||
}
|
||||
|
||||
run_with_timeout() {
|
||||
local status supervisor_pid
|
||||
setsid timeout --foreground --signal=TERM \
|
||||
--kill-after="${TIMEOUT_KILL_AFTER_SECONDS}s" \
|
||||
"${CASE_TIMEOUT_SECONDS}s" "$@" &
|
||||
supervisor_pid=$!
|
||||
set +e
|
||||
wait "${supervisor_pid}"
|
||||
status=$?
|
||||
set -e
|
||||
if [[ ${status} -eq 124 || ${status} -eq 137 ]]; then
|
||||
kill -TERM -- "-${supervisor_pid}" 2>/dev/null || true
|
||||
sleep "${TIMEOUT_KILL_AFTER_SECONDS}"
|
||||
kill -KILL -- "-${supervisor_pid}" 2>/dev/null || true
|
||||
fi
|
||||
return "${status}"
|
||||
}
|
||||
|
||||
run_case() {
|
||||
local phase=$1
|
||||
local repeat=$2
|
||||
local case_definition=$3
|
||||
local package_root=$4
|
||||
local measured_rounds=$5
|
||||
local label implementation group_size num_in_group
|
||||
IFS=: read -r label implementation group_size num_in_group <<< "${case_definition}"
|
||||
local phase_dir="${RESULTS_DIR}/${phase}-logs"
|
||||
local phase_result="${RESULTS_DIR}/${phase}-runs.jsonl"
|
||||
local raw_result="${RESULTS_DIR}/tmp/${phase}-${label}-${repeat}.jsonl"
|
||||
local raw_evidence="${RESULTS_DIR}/tmp/${phase}-${label}-${repeat}.evidence.json"
|
||||
local raw_log="${phase_dir}/${label}-${repeat}.log.partial"
|
||||
local final_log="${phase_dir}/${label}-${repeat}.log"
|
||||
local runtime_identity=latest
|
||||
local evidence_mode=actual
|
||||
local backend_args=()
|
||||
mkdir -p "${phase_dir}"
|
||||
if [[ "${phase}" == native-v018 ]]; then
|
||||
runtime_identity=native_v018
|
||||
fi
|
||||
if [[ "${group_size}" -gt 0 ]]; then
|
||||
backend_args=(--offload-backend prefetch)
|
||||
else
|
||||
evidence_mode=baseline
|
||||
fi
|
||||
|
||||
printf 'phase=%s repeat=%s case=%s implementation=%s\n' \
|
||||
"${phase}" "${repeat}" "${label}" "${implementation}" | tee -a "${RESULTS_DIR}/runner.log"
|
||||
set +e
|
||||
OFFLOAD_EVIDENCE_PATH="${raw_evidence}" \
|
||||
OFFLOAD_EVIDENCE_MODE="${evidence_mode}" \
|
||||
RUNTIME_IDENTITY="${runtime_identity}" \
|
||||
PYTHONPATH="${EVIDENCE_HOOK_ROOT}:${package_root}:${CANN_PYTHONPATH}" \
|
||||
run_with_timeout \
|
||||
python "${SCRIPT_DIR}/bench_one.py" \
|
||||
--model "${MODEL_PATH}" \
|
||||
--label "${label}" \
|
||||
--offload-group-size "${group_size}" \
|
||||
--offload-num-in-group "${num_in_group}" "${backend_args[@]}" \
|
||||
--prompt-count 8 \
|
||||
--input-tokens 128 \
|
||||
--max-tokens 64 \
|
||||
--max-model-len 512 \
|
||||
--warmup-rounds 1 \
|
||||
--measured-rounds "${measured_rounds}" \
|
||||
--seed 0 \
|
||||
--result-file "${raw_result}" \
|
||||
--gpu-memory-utilization 0.5 \
|
||||
--npu-index "${NPU_INDEX}" > "${raw_log}" 2>&1
|
||||
local status=$?
|
||||
set -e
|
||||
if [[ ${status} -ne 0 ]]; then
|
||||
printf 'case failed with status=%s; retained partial log %s\n' "${status}" "${raw_log}" >&2
|
||||
return "${status}"
|
||||
fi
|
||||
RUNTIME_IDENTITY="${runtime_identity}" finalize_case \
|
||||
"${raw_result}" "${raw_log}" "${raw_evidence}" \
|
||||
"${phase_result}" "${final_log}"
|
||||
sleep 3
|
||||
}
|
||||
|
||||
select_package_root() {
|
||||
local package_mode=$1
|
||||
local case_definition=$2
|
||||
if [[ "${package_mode}" == native ]]; then
|
||||
printf '%s' "${CURRENT_V018_SOURCE_ROOT}"
|
||||
elif [[ "${package_mode}" == current && "${case_definition}" == *":current:"* ]]; then
|
||||
printf '%s' "${OVERLAY_ROOT}"
|
||||
else
|
||||
printf '%s' "${OFFICIAL_PACKAGE_PARENT}"
|
||||
fi
|
||||
}
|
||||
|
||||
run_rotated_schedule() {
|
||||
local phase=$1
|
||||
local package_mode=$2
|
||||
local measured_rounds=$3
|
||||
local case_dispatcher=$4
|
||||
shift 4
|
||||
local cases=("$@") repeat offset index package_root
|
||||
for ((repeat = 0; repeat < 3; repeat++)); do
|
||||
for ((offset = 0; offset < ${#cases[@]}; offset++)); do
|
||||
index=$(((repeat + offset) % ${#cases[@]}))
|
||||
package_root="$(select_package_root "${package_mode}" "${cases[index]}")"
|
||||
"${case_dispatcher}" "${phase}" "${repeat}" "${cases[index]}" \
|
||||
"${package_root}" "${measured_rounds}"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
record_test_launch() {
|
||||
: "${RUNNER_TEST_TRACE:?RUNNER_TEST_TRACE must be set for schedule dry runs}"
|
||||
local phase=$1 repeat=$2 case_definition=$3 package_root=$4 measured_rounds=$5
|
||||
local label implementation group_size num_in_group rendered_args
|
||||
local evidence_mode=actual
|
||||
local args=(--offload-group-size)
|
||||
IFS=: read -r label implementation group_size num_in_group <<< "${case_definition}"
|
||||
args+=("${group_size}" --offload-num-in-group "${num_in_group}")
|
||||
if [[ "${group_size}" -gt 0 ]]; then
|
||||
args+=(--offload-backend prefetch)
|
||||
else
|
||||
evidence_mode=baseline
|
||||
fi
|
||||
args+=(--measured-rounds "${measured_rounds}")
|
||||
printf -v rendered_args '%s ' "${args[@]}"
|
||||
printf 'phase=%s repeat=%s case=%s implementation=%s\n' \
|
||||
"${phase}" "${repeat}" "${label}" "${implementation}" \
|
||||
>> "${RESULTS_DIR}/runner.log"
|
||||
printf 'launch\t%s\t%s\t%s\t%s\t%s\t%s\n' \
|
||||
"${label}" "${phase}-${label}-${repeat}.jsonl" "${rendered_args% }" \
|
||||
"${EVIDENCE_HOOK_ROOT}:${package_root}:${CANN_PYTHONPATH}" \
|
||||
"${FAKE_VENV:-}" "${evidence_mode}" \
|
||||
>> "${RUNNER_TEST_TRACE}"
|
||||
}
|
||||
|
||||
run_same_runtime_phase() {
|
||||
local case_dispatcher=${1:-run_case}
|
||||
verify_latest_runtime_identity
|
||||
run_health_before_phase same-runtime
|
||||
run_rotated_schedule same-runtime current 3 "${case_dispatcher}" \
|
||||
"${SAME_RUNTIME_CASES[@]}"
|
||||
}
|
||||
|
||||
run_long_window_phase() {
|
||||
local case_dispatcher=${1:-run_case}
|
||||
verify_latest_runtime_identity
|
||||
run_health_before_phase long-window
|
||||
run_rotated_schedule long-window current 10 "${case_dispatcher}" \
|
||||
"${LONG_WINDOW_CASES[@]}"
|
||||
}
|
||||
|
||||
verify_native_deployment() {
|
||||
local expected_offloader expected_runner actual_offloader actual_runner
|
||||
local actual_source_tree actual_worker
|
||||
expected_offloader="$(python - "${RESULTS_DIR}/environment-manifest.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
print(json.load(open(sys.argv[1], encoding="utf-8"))["project_source"]["offloader_tree_sha256"])
|
||||
PY
|
||||
)"
|
||||
expected_runner="$(python - "${RESULTS_DIR}/environment-manifest.json" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
print(json.load(open(sys.argv[1], encoding="utf-8"))["project_source"]["runner_sha256"])
|
||||
PY
|
||||
)"
|
||||
actual_offloader="$(hash_tree "${CURRENT_V018_SOURCE_ROOT}/vllm_ascend/model_executor/offloader")"
|
||||
actual_worker="$(
|
||||
sha256sum "${CURRENT_V018_SOURCE_ROOT}/vllm_ascend/worker/model_runner_v1.py" |
|
||||
awk '{print $1}'
|
||||
)"
|
||||
actual_runner="$(sha256sum "${SCRIPT_DIR}/run_large_model_parameter_comparison.sh" | awk '{print $1}')"
|
||||
actual_source_tree="$(hash_tree "${CURRENT_V018_SOURCE_ROOT}/vllm_ascend")"
|
||||
if [[
|
||||
"${actual_offloader}" != "${expected_offloader}" ||
|
||||
"${actual_worker}" != "${PROJECT_WORKER_HASH}" ||
|
||||
"${actual_runner}" != "${expected_runner}" ||
|
||||
"${actual_source_tree}" != "${NATIVE_SOURCE_TREE_SHA256}"
|
||||
]]; then
|
||||
echo "deployed v0.18 source tree, worker, offloader, or runner does not match environment manifest" >&2
|
||||
exit 3
|
||||
fi
|
||||
}
|
||||
|
||||
run_native_v018_phase() {
|
||||
local case_dispatcher=${1:-run_case}
|
||||
run_health_before_phase native-v018
|
||||
verify_native_deployment
|
||||
|
||||
(
|
||||
source "${CURRENT_V018_VENV_ACTIVATE}"
|
||||
bind_run_cache
|
||||
python -m pip freeze > "${RESULTS_DIR}/native-v018-package-versions.txt"
|
||||
run_rotated_schedule native-v018 native 3 "${case_dispatcher}" \
|
||||
"${NATIVE_V018_CASES[@]}"
|
||||
)
|
||||
}
|
||||
|
||||
if [[ "${RUNNER_TEST_FINALIZE_CASE:-0}" == 1 ]]; then
|
||||
run_case "same-runtime" 0 "current_14_1:current:14:1" "${OVERLAY_ROOT}" 3
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${RUNNER_TEST_NATIVE_GUARD_ONLY:-0}" == 1 ]]; then
|
||||
verify_native_deployment
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case_dispatcher=run_case
|
||||
if [[ "${RUNNER_TEST_SCHEDULE_ONLY:-0}" == 1 ]]; then
|
||||
case_dispatcher=record_test_launch
|
||||
fi
|
||||
run_same_runtime_phase "${case_dispatcher}"
|
||||
run_long_window_phase "${case_dispatcher}"
|
||||
run_native_v018_phase "${case_dispatcher}"
|
||||
verify_latest_runtime_identity
|
||||
verify_native_deployment
|
||||
bash "${SCRIPT_DIR}/check_npu_health.sh" > "${RESULTS_DIR}/npu-health-after.log" 2>&1
|
||||
npu-smi info > "${RESULTS_DIR}/npu-state-after.log" 2>&1
|
||||
df -h "${MODEL_PATH}" "${RESULTS_DIR}" > "${RESULTS_DIR}/disk-space-after.log"
|
||||
|
||||
"${LATEST_PYTHON}" "${SCRIPT_DIR}/analyze_large_model_parameter_comparison.py" \
|
||||
--results-dir "${RESULTS_DIR}" --model-path "${MODEL_PATH}"
|
||||
|
||||
mkdir -p "${RESULTS_DIR}/reproduction"
|
||||
cp "${SCRIPT_DIR}/run_large_model_parameter_comparison.sh" \
|
||||
"${SCRIPT_DIR}/analyze_large_model_parameter_comparison.py" \
|
||||
"${SCRIPT_DIR}/bench_one.py" \
|
||||
"${REPO_ROOT}/tests/model_executor/offloader/test_large_model_parameter_comparison.py" \
|
||||
"${RESULTS_DIR}/reproduction/"
|
||||
cp -a "${SCRIPT_DIR}/qwen_offload_evidence_hook" \
|
||||
"${RESULTS_DIR}/reproduction/"
|
||||
results_parent="$(dirname "${RESULTS_DIR}")"
|
||||
results_name="$(basename "${RESULTS_DIR}")"
|
||||
(
|
||||
cd "${results_parent}"
|
||||
find "${results_name}" -type f ! -name SHA256SUMS -print0 | sort -z | xargs -0 sha256sum > "${results_name}/SHA256SUMS"
|
||||
)
|
||||
if [[ "${RUNNER_TEST_SCHEDULE_ONLY:-0}" == 1 ]]; then
|
||||
printf 'checksum\n' >> "${RUNNER_TEST_TRACE:?RUNNER_TEST_TRACE must be set for schedule dry runs}"
|
||||
fi
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CANN_ENV=${CANN_ENV:-/data/Ascend/cann/set_env.sh}
|
||||
ATB_ENV=${ATB_ENV:-/data/Ascend/nnal/atb/set_env.sh}
|
||||
VENV_ACTIVATE=${VENV_ACTIVATE:-/data/vllm-ascend-v0.18.0/.venv/bin/activate}
|
||||
|
||||
source "${CANN_ENV}"
|
||||
if [[ -f "${ATB_ENV}" ]]; then
|
||||
source "${ATB_ENV}"
|
||||
fi
|
||||
source "${VENV_ACTIVATE}"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
MODEL_PATH=${MODEL_PATH:?MODEL_PATH must point to the model}
|
||||
RESULTS_DIR=${RESULTS_DIR:-/data/v018-parameter-optimization}
|
||||
COOLDOWN_SECONDS=${COOLDOWN_SECONDS:-3}
|
||||
NPU_INDEX=${NPU_INDEX:-$(python "${SCRIPT_DIR}/detect_npu_index.py")}
|
||||
NPU_CHIP_INDEX=${NPU_CHIP_INDEX:-0}
|
||||
ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
|
||||
PROMPT_COUNT=${PROMPT_COUNT:-8}
|
||||
INPUT_TOKENS=${INPUT_TOKENS:-128}
|
||||
MAX_TOKENS=${MAX_TOKENS:-64}
|
||||
MAX_MODEL_LEN=${MAX_MODEL_LEN:-512}
|
||||
WARMUP_ROUNDS=${WARMUP_ROUNDS:-1}
|
||||
MEASURED_ROUNDS=${MEASURED_ROUNDS:-3}
|
||||
CONFIRMATION_ROUNDS=${CONFIRMATION_ROUNDS:-10}
|
||||
GPU_MEMORY_UTILIZATION=${GPU_MEMORY_UTILIZATION:-0.5}
|
||||
SEED=${SEED:-0}
|
||||
|
||||
VLLM_ASCEND_CUSTOM_OP_ROOT=${VLLM_ASCEND_CUSTOM_OP_ROOT-/data/vllm-ascend-v0.18.0/vllm_ascend/_cann_ops_custom/vendors/vllm-ascend}
|
||||
if [[ -n "${VLLM_ASCEND_CUSTOM_OP_ROOT}" ]]; then
|
||||
export ASCEND_CUSTOM_OPP_PATH="${VLLM_ASCEND_CUSTOM_OP_ROOT}${ASCEND_CUSTOM_OPP_PATH:+:${ASCEND_CUSTOM_OPP_PATH}}"
|
||||
export LD_LIBRARY_PATH="${VLLM_ASCEND_CUSTOM_OP_ROOT}/op_api/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
||||
fi
|
||||
|
||||
export PYTHONPATH="${REPO_ROOT}${PYTHONPATH:+:${PYTHONPATH}}"
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export ASCEND_RT_VISIBLE_DEVICES
|
||||
export NPU_INDEX NPU_CHIP_INDEX
|
||||
export SOC_VERSION=${SOC_VERSION:-ascend910b1}
|
||||
export TASK_QUEUE_ENABLE=${TASK_QUEUE_ENABLE:-1}
|
||||
export OMP_NUM_THREADS=${OMP_NUM_THREADS:-1}
|
||||
export PYTORCH_NPU_ALLOC_CONF=${PYTORCH_NPU_ALLOC_CONF:-expandable_segments:True}
|
||||
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-/data/cache-v018-parameter-optimization}
|
||||
export HF_HOME=${HF_HOME:-/data/huggingface-cache}
|
||||
export MODELSCOPE_CACHE=${MODELSCOPE_CACHE:-/data/modelscope-cache}
|
||||
export VLLM_CACHE_ROOT=${VLLM_CACHE_ROOT:-/data/vllm-cache-v018-parameter-optimization}
|
||||
export TRITON_CACHE_DIR=${TRITON_CACHE_DIR:-/data/triton-cache-v018-parameter-optimization}
|
||||
export PYTHONNOUSERSITE=1
|
||||
|
||||
if [[ ${SKIP_NPU_HEALTH_CHECK:-0} != 1 ]]; then
|
||||
bash "${SCRIPT_DIR}/check_npu_health.sh"
|
||||
fi
|
||||
|
||||
if [[ -e "${RESULTS_DIR}/performance-runs.jsonl" ]]; then
|
||||
echo "RESULTS_DIR already contains performance-runs.jsonl: ${RESULTS_DIR}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mkdir -p \
|
||||
"${RESULTS_DIR}/logs" \
|
||||
"${RESULTS_DIR}/confirmation-logs" \
|
||||
"${RESULTS_DIR}/reproduction"
|
||||
|
||||
MAIN_RESULTS="${RESULTS_DIR}/performance-runs.jsonl"
|
||||
CONFIRMATION_RESULTS="${RESULTS_DIR}/confirmation-runs.jsonl"
|
||||
: > "${MAIN_RESULTS}"
|
||||
: > "${CONFIRMATION_RESULTS}"
|
||||
|
||||
SOURCE_BRANCH=${SOURCE_BRANCH:-$(git -C "${REPO_ROOT}" branch --show-current 2>/dev/null || printf unknown)}
|
||||
SOURCE_COMMIT=${SOURCE_COMMIT:-$(git -C "${REPO_ROOT}" rev-parse --short HEAD 2>/dev/null || printf unknown)}
|
||||
VLLM_VERSION=$(python -c 'from importlib.metadata import version; print(version("vllm"))')
|
||||
VLLM_ASCEND_VERSION=$(python -c 'from importlib.metadata import version; print(version("vllm-ascend"))')
|
||||
CANN_VERSION=$(basename -- "$(realpath "$(dirname -- "${CANN_ENV}")")")
|
||||
HARDWARE_LABEL=${HARDWARE_LABEL:-"Ascend NPU ${NPU_INDEX}, logical device ${ASCEND_RT_VISIBLE_DEVICES}"}
|
||||
|
||||
{
|
||||
printf 'captured_at_utc='
|
||||
date -u +%FT%TZ
|
||||
uname -a
|
||||
printf '\n[Python packages]\n'
|
||||
python -V
|
||||
python -m pip show vllm vllm-ascend torch torch-npu triton-ascend
|
||||
printf '\n[NPU]\n'
|
||||
npu-smi info
|
||||
printf '\n[Current source]\n'
|
||||
printf 'branch=%s\ncommit=%s\n' "${SOURCE_BRANCH}" "${SOURCE_COMMIT}"
|
||||
sha256sum \
|
||||
"${REPO_ROOT}"/vllm_ascend/model_executor/offloader/*.py \
|
||||
"${REPO_ROOT}"/vllm_ascend/worker/model_runner_v1.py
|
||||
printf '\n[CANN]\n%s\n' "$(realpath "$(dirname -- "${CANN_ENV}")")"
|
||||
} > "${RESULTS_DIR}/environment.txt" 2>&1
|
||||
|
||||
run_case() {
|
||||
local phase=$1
|
||||
local label=$2
|
||||
local group_size=$3
|
||||
local num_in_group=$4
|
||||
local repeat=$5
|
||||
local measured_rounds=$6
|
||||
local result_file="${MAIN_RESULTS}"
|
||||
local log_dir="${RESULTS_DIR}/logs"
|
||||
local backend_args=()
|
||||
|
||||
if [[ "${phase}" == confirmation ]]; then
|
||||
result_file="${CONFIRMATION_RESULTS}"
|
||||
log_dir="${RESULTS_DIR}/confirmation-logs"
|
||||
fi
|
||||
if [[ "${group_size}" -gt 0 ]]; then
|
||||
backend_args=(--offload-backend prefetch)
|
||||
fi
|
||||
|
||||
printf 'phase=%s case=%s repeat=%s\n' "${phase}" "${label}" "${repeat}"
|
||||
python "${SCRIPT_DIR}/bench_one.py" \
|
||||
--model "${MODEL_PATH}" \
|
||||
--label "${label}" \
|
||||
"${backend_args[@]}" \
|
||||
--offload-group-size "${group_size}" \
|
||||
--offload-num-in-group "${num_in_group}" \
|
||||
--prompt-count "${PROMPT_COUNT}" \
|
||||
--input-tokens "${INPUT_TOKENS}" \
|
||||
--max-tokens "${MAX_TOKENS}" \
|
||||
--max-model-len "${MAX_MODEL_LEN}" \
|
||||
--warmup-rounds "${WARMUP_ROUNDS}" \
|
||||
--measured-rounds "${measured_rounds}" \
|
||||
--seed "${SEED}" \
|
||||
--result-file "${result_file}" \
|
||||
--npu-index "${NPU_INDEX}" \
|
||||
--gpu-memory-utilization "${GPU_MEMORY_UTILIZATION}" \
|
||||
2>&1 | tee "${log_dir}/${label}_repeat_${repeat}.log"
|
||||
sleep "${COOLDOWN_SECONDS}"
|
||||
}
|
||||
|
||||
# Five repeats around the low-density operating region.
|
||||
FOCUS_CASES=(
|
||||
"baseline_0_1:0:1"
|
||||
"group_28_num_1:28:1"
|
||||
"group_14_num_1:14:1"
|
||||
"group_28_num_2:28:2"
|
||||
"group_8_num_1:8:1"
|
||||
)
|
||||
for ((repeat=0; repeat<5; repeat++)); do
|
||||
start=$((repeat % ${#FOCUS_CASES[@]}))
|
||||
for ((offset=0; offset<${#FOCUS_CASES[@]}; offset++)); do
|
||||
index=$(((start + offset) % ${#FOCUS_CASES[@]}))
|
||||
IFS=: read -r label group_size num_in_group <<< "${FOCUS_CASES[index]}"
|
||||
run_case main "${label}" "${group_size}" "${num_in_group}" \
|
||||
"${repeat}" "${MEASURED_ROUNDS}"
|
||||
done
|
||||
done
|
||||
|
||||
# Three repeats cover the capacity-heavy end of the parameter space.
|
||||
CAPACITY_CASES=(
|
||||
"group_7_num_1:7:1"
|
||||
"group_14_num_2:14:2"
|
||||
"group_8_num_2:8:2"
|
||||
"group_4_num_1:4:1"
|
||||
"group_2_num_1:2:1"
|
||||
"group_4_num_2:4:2"
|
||||
)
|
||||
for ((repeat=0; repeat<3; repeat++)); do
|
||||
start=$((repeat % ${#CAPACITY_CASES[@]}))
|
||||
for ((offset=0; offset<${#CAPACITY_CASES[@]}; offset++)); do
|
||||
index=$(((start + offset) % ${#CAPACITY_CASES[@]}))
|
||||
IFS=: read -r label group_size num_in_group <<< "${CAPACITY_CASES[index]}"
|
||||
run_case main "${label}" "${group_size}" "${num_in_group}" \
|
||||
"${repeat}" "${MEASURED_ROUNDS}"
|
||||
done
|
||||
done
|
||||
|
||||
# A longer Latin-square confirmation separates small effects from runtime noise.
|
||||
CONFIRMATION_CASES=(
|
||||
"baseline_0_1:0:1"
|
||||
"group_28_num_1:28:1"
|
||||
"group_14_num_1:14:1"
|
||||
)
|
||||
for ((repeat=0; repeat<3; repeat++)); do
|
||||
start=$((repeat % ${#CONFIRMATION_CASES[@]}))
|
||||
for ((offset=0; offset<${#CONFIRMATION_CASES[@]}; offset++)); do
|
||||
index=$(((start + offset) % ${#CONFIRMATION_CASES[@]}))
|
||||
IFS=: read -r label group_size num_in_group \
|
||||
<<< "${CONFIRMATION_CASES[index]}"
|
||||
run_case confirmation "${label}" "${group_size}" "${num_in_group}" \
|
||||
"${repeat}" "${CONFIRMATION_ROUNDS}"
|
||||
done
|
||||
done
|
||||
|
||||
python "${SCRIPT_DIR}/analyze_parameter_optimization.py" \
|
||||
--results-dir "${RESULTS_DIR}" \
|
||||
--branch "${SOURCE_BRANCH}" \
|
||||
--commit "${SOURCE_COMMIT}" \
|
||||
--vllm-version "${VLLM_VERSION}" \
|
||||
--vllm-ascend-version "${VLLM_ASCEND_VERSION}" \
|
||||
--cann-version "${CANN_VERSION}" \
|
||||
--hardware "${HARDWARE_LABEL}"
|
||||
|
||||
cp \
|
||||
"${SCRIPT_DIR}/run_parameter_optimization_benchmark.sh" \
|
||||
"${SCRIPT_DIR}/analyze_parameter_optimization.py" \
|
||||
"${SCRIPT_DIR}/bench_one.py" \
|
||||
"${RESULTS_DIR}/reproduction/"
|
||||
|
||||
(
|
||||
cd -- "$(dirname -- "${RESULTS_DIR}")"
|
||||
results_name="$(basename -- "${RESULTS_DIR}")"
|
||||
find "${results_name}" -type f ! -name SHA256SUMS -print0 \
|
||||
| sort -z \
|
||||
| xargs -0 sha256sum > "${results_name}/SHA256SUMS"
|
||||
)
|
||||
|
||||
printf 'Results: %s\n' "${RESULTS_DIR}"
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CANN_ENV=${CANN_ENV:-/usr/local/Ascend/cann-8.5.1/cann-8.5.1/set_env.sh}
|
||||
VENV_ACTIVATE=${VENV_ACTIVATE:-/data/vllm-ascend-v0.18.0/.venv/bin/activate}
|
||||
source "${CANN_ENV}"
|
||||
source "${VENV_ACTIVATE}"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
|
||||
VLLM_ASCEND_CUSTOM_OP_ROOT=${VLLM_ASCEND_CUSTOM_OP_ROOT-/data/vllm-ascend-v0.18.0/vllm_ascend/_cann_ops_custom/vendors/vllm-ascend}
|
||||
if [[ -n "${VLLM_ASCEND_CUSTOM_OP_ROOT}" ]]; then
|
||||
export ASCEND_CUSTOM_OPP_PATH="${VLLM_ASCEND_CUSTOM_OP_ROOT}${ASCEND_CUSTOM_OPP_PATH:+:${ASCEND_CUSTOM_OPP_PATH}}"
|
||||
export LD_LIBRARY_PATH="${VLLM_ASCEND_CUSTOM_OP_ROOT}/op_api/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}"
|
||||
fi
|
||||
export CANN_ENV VENV_ACTIVATE VLLM_ASCEND_CUSTOM_OP_ROOT
|
||||
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export VLLM_USE_MODELSCOPE=True
|
||||
export ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
export NPU_INDEX=${NPU_INDEX:-$(python "${SCRIPT_DIR}/detect_npu_index.py")}
|
||||
export NPU_CHIP_INDEX=${NPU_CHIP_INDEX:-0}
|
||||
export XDG_CACHE_HOME=${XDG_CACHE_HOME:-/data/cache}
|
||||
export HF_HOME=${HF_HOME:-/data/huggingface-cache}
|
||||
export MODELSCOPE_CACHE=${MODELSCOPE_CACHE:-/data/modelscope-cache}
|
||||
export TRITON_CACHE_DIR=${TRITON_CACHE_DIR:-/data/triton-cache-cann851}
|
||||
|
||||
MODEL_PATH=${MODEL_PATH:?MODEL_PATH must point to the model}
|
||||
REPRO_RESULTS_DIR=${REPRO_RESULTS_DIR:-/data/reproduction-results}
|
||||
PERFORMANCE_DIR="${REPRO_RESULTS_DIR}/performance"
|
||||
ACCURACY_DIR="${REPRO_RESULTS_DIR}/accuracy"
|
||||
STARTED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
mkdir -p "${PERFORMANCE_DIR}" "${ACCURACY_DIR}"
|
||||
|
||||
RESULTS_DIR="${PERFORMANCE_DIR}" bash "${SCRIPT_DIR}/run_benchmark_all.sh"
|
||||
RESULTS_DIR="${ACCURACY_DIR}" bash "${SCRIPT_DIR}/run_accuracy_gsm8k_gpqa.sh"
|
||||
|
||||
FINISHED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
MANIFEST_PATH="${REPRO_RESULTS_DIR}/reproduction-manifest.json"
|
||||
export REPO_ROOT MODEL_PATH REPRO_RESULTS_DIR PERFORMANCE_DIR ACCURACY_DIR
|
||||
export STARTED_AT FINISHED_AT MANIFEST_PATH
|
||||
|
||||
python - <<'PY'
|
||||
import importlib.metadata
|
||||
import json
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def package_version(distribution):
|
||||
try:
|
||||
return importlib.metadata.version(distribution)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
source_commit = subprocess.run(
|
||||
["git", "-C", "/data/vllm-ascend-v0.18.0", "rev-parse", "HEAD"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
performance_dir = Path(os.environ["PERFORMANCE_DIR"])
|
||||
accuracy_dir = Path(os.environ["ACCURACY_DIR"])
|
||||
repo_root = Path(os.environ["REPO_ROOT"])
|
||||
performance_command = shlex.join([
|
||||
"env",
|
||||
f"RESULTS_DIR={performance_dir}",
|
||||
"bash",
|
||||
str(repo_root / "scripts" / "run_benchmark_all.sh"),
|
||||
])
|
||||
accuracy_command = shlex.join([
|
||||
"env",
|
||||
f"RESULTS_DIR={accuracy_dir}",
|
||||
"bash",
|
||||
str(repo_root / "scripts" / "run_accuracy_gsm8k_gpqa.sh"),
|
||||
])
|
||||
payload = {
|
||||
"started_at": os.environ["STARTED_AT"],
|
||||
"finished_at": os.environ["FINISHED_AT"],
|
||||
"commands": [
|
||||
performance_command,
|
||||
accuracy_command,
|
||||
],
|
||||
"package_versions": {
|
||||
name: package_version(name)
|
||||
for name in (
|
||||
"vllm", "vllm-ascend", "torch", "torch-npu", "triton-ascend"
|
||||
)
|
||||
},
|
||||
"source_commit": source_commit,
|
||||
"model_path": os.environ["MODEL_PATH"],
|
||||
"artifacts": {
|
||||
"performance_jsonl": str(performance_dir / "performance-runs.jsonl"),
|
||||
"performance_csv": str(performance_dir / "performance-summary.csv"),
|
||||
"performance_markdown": str(performance_dir / "performance-summary.md"),
|
||||
"performance_json": str(performance_dir / "performance-summary.json"),
|
||||
"accuracy_directory": str(accuracy_dir),
|
||||
"accuracy_summary_json": str(accuracy_dir / "accuracy-summary.json"),
|
||||
"accuracy_summary_csv": str(accuracy_dir / "accuracy-summary.csv"),
|
||||
"accuracy_summary_markdown": str(accuracy_dir / "accuracy-summary.md"),
|
||||
},
|
||||
}
|
||||
manifest = Path(os.environ["MANIFEST_PATH"])
|
||||
manifest.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
||||
print(manifest)
|
||||
PY
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import os
|
||||
import time
|
||||
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
model = "/data/vllm-ascend-smoke/models/Qwen/Qwen2.5-0.5B-Instruct"
|
||||
print("SMOKE_MODEL", model, flush=True)
|
||||
print(
|
||||
"SMOKE_ENV",
|
||||
{k: os.environ.get(k) for k in ["VLLM_TARGET_DEVICE", "ASCEND_RT_VISIBLE_DEVICES"]},
|
||||
flush=True,
|
||||
)
|
||||
start = time.time()
|
||||
llm = LLM(
|
||||
model=model,
|
||||
trust_remote_code=True,
|
||||
dtype="float16",
|
||||
max_model_len=256,
|
||||
max_num_seqs=1,
|
||||
max_num_batched_tokens=256,
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
gpu_memory_utilization=0.6,
|
||||
)
|
||||
print("SMOKE_LOAD_SECONDS", round(time.time() - start, 2), flush=True)
|
||||
outputs = llm.generate(
|
||||
["Hello, briefly introduce vLLM Ascend."],
|
||||
SamplingParams(temperature=0.0, max_tokens=16),
|
||||
)
|
||||
for out in outputs:
|
||||
print("SMOKE_PROMPT", out.prompt, flush=True)
|
||||
print("SMOKE_OUTPUT", out.outputs[0].text, flush=True)
|
||||
print("SMOKE_OK", flush=True)
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
set -o pipefail
|
||||
source /usr/local/Ascend/ascend-toolkit/set_env.sh 2>/dev/null || source /usr/local/Ascend/cann/set_env.sh 2>/dev/null || true
|
||||
source /data/vllm-ascend-smoke/venv/bin/activate
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
export PYTORCH_NPU_ALLOC_CONF=${PYTORCH_NPU_ALLOC_CONF:-expandable_segments:True}
|
||||
export VLLM_LOGGING_LEVEL=${VLLM_LOGGING_LEVEL:-INFO}
|
||||
python /data/vllm-ascend-smoke/run_smoke.py 2>&1 | tee /data/vllm-ascend-smoke/logs/smoke-rerun.log
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
from vllm import LLM
|
||||
|
||||
model = "/data/vllm-ascend-smoke/models/Qwen/Qwen2.5-0.5B-Instruct"
|
||||
try:
|
||||
LLM(
|
||||
model=model,
|
||||
trust_remote_code=True,
|
||||
dtype="float16",
|
||||
max_model_len=128,
|
||||
max_num_seqs=1,
|
||||
max_num_batched_tokens=128,
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
gpu_memory_utilization=0.6,
|
||||
offload_group_size=2,
|
||||
offload_num_in_group=3,
|
||||
)
|
||||
except ValueError as exc:
|
||||
text = str(exc)
|
||||
print("SMOKE_INVALID_ERROR", text, flush=True)
|
||||
if "offload_num_in_group" in text and "offload_group_size" in text:
|
||||
print("SMOKE_INVALID_OK", flush=True)
|
||||
raise SystemExit(0)
|
||||
raise
|
||||
raise RuntimeError("invalid offload config unexpectedly succeeded")
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
set -o pipefail
|
||||
source /usr/local/Ascend/ascend-toolkit/set_env.sh 2>/dev/null || source /usr/local/Ascend/cann/set_env.sh 2>/dev/null || true
|
||||
source /data/vllm-ascend-smoke/venv/bin/activate
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
export PYTORCH_NPU_ALLOC_CONF=${PYTORCH_NPU_ALLOC_CONF:-expandable_segments:True}
|
||||
export VLLM_LOGGING_LEVEL=${VLLM_LOGGING_LEVEL:-INFO}
|
||||
python /data/vllm-ascend-smoke/run_smoke_invalid_offload.py 2>&1 | tee /data/vllm-ascend-smoke/logs/smoke-invalid-offload.log
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import os
|
||||
import time
|
||||
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
model = "/data/vllm-ascend-smoke/models/Qwen/Qwen2.5-0.5B-Instruct"
|
||||
print("SMOKE_MODEL", model, flush=True)
|
||||
print(
|
||||
"SMOKE_ENV",
|
||||
{k: os.environ.get(k) for k in ["VLLM_TARGET_DEVICE", "ASCEND_RT_VISIBLE_DEVICES", "PYTHONPATH"]},
|
||||
flush=True,
|
||||
)
|
||||
start = time.time()
|
||||
llm = LLM(
|
||||
model=model,
|
||||
trust_remote_code=True,
|
||||
dtype="float16",
|
||||
max_model_len=256,
|
||||
max_num_seqs=1,
|
||||
max_num_batched_tokens=256,
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
gpu_memory_utilization=0.6,
|
||||
offload_group_size=4,
|
||||
offload_num_in_group=1,
|
||||
)
|
||||
print("SMOKE_LOAD_SECONDS", round(time.time() - start, 2), flush=True)
|
||||
outputs = llm.generate(
|
||||
["Hello, briefly introduce vLLM Ascend."],
|
||||
SamplingParams(temperature=0.0, max_tokens=16),
|
||||
)
|
||||
for out in outputs:
|
||||
print("SMOKE_PROMPT", out.prompt, flush=True)
|
||||
print("SMOKE_OUTPUT", out.outputs[0].text, flush=True)
|
||||
print("SMOKE_OK_OFFLOAD", flush=True)
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
#!/usr/bin/env bash
|
||||
set -o pipefail
|
||||
source /usr/local/Ascend/ascend-toolkit/set_env.sh 2>/dev/null || source /usr/local/Ascend/cann/set_env.sh 2>/dev/null || true
|
||||
source /data/vllm-ascend-smoke/venv/bin/activate
|
||||
if [[ "${USE_DEV_WORKTREE:-0}" == "1" ]]; then
|
||||
export PYTHONPATH=/data/vllm-ascend-dev/worktree${PYTHONPATH:+:$PYTHONPATH}
|
||||
fi
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
export PYTORCH_NPU_ALLOC_CONF=${PYTORCH_NPU_ALLOC_CONF:-expandable_segments:True}
|
||||
export VLLM_LOGGING_LEVEL=${VLLM_LOGGING_LEVEL:-INFO}
|
||||
python /data/vllm-ascend-smoke/run_smoke_offload.py 2>&1 | tee /data/vllm-ascend-smoke/logs/smoke-offload-installed.log
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source /usr/local/Ascend/ascend-toolkit/set_env.sh 2>/dev/null || source /usr/local/Ascend/cann/set_env.sh 2>/dev/null || true
|
||||
source /data/vllm-ascend-smoke/venv/bin/activate
|
||||
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
export PYTORCH_NPU_ALLOC_CONF=${PYTORCH_NPU_ALLOC_CONF:-expandable_segments:True}
|
||||
export VLLM_LOGGING_LEVEL=${VLLM_LOGGING_LEVEL:-INFO}
|
||||
|
||||
LOG=${LOG:-/data/vllm-ascend-smoke/logs/stability-100-offload-$(date +%Y%m%d-%H%M%S).log}
|
||||
python /data/vllm-ascend-smoke/stability_100.py 2>&1 | tee "$LOG"
|
||||
grep -H "STABILITY_RESULT" "$LOG"
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
source /usr/local/Ascend/ascend-toolkit/set_env.sh 2>/dev/null || source /usr/local/Ascend/cann/set_env.sh 2>/dev/null || true
|
||||
source /data/vllm-ascend-smoke/venv/bin/activate
|
||||
|
||||
export VLLM_TARGET_DEVICE=ascend
|
||||
export ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}
|
||||
export PYTORCH_NPU_ALLOC_CONF=${PYTORCH_NPU_ALLOC_CONF:-expandable_segments:True}
|
||||
export VLLM_LOGGING_LEVEL=${VLLM_LOGGING_LEVEL:-INFO}
|
||||
|
||||
MODEL=${MODEL:-/data/vllm-ascend-smoke/models/Qwen/Qwen2.5-0.5B-Instruct}
|
||||
ITERATIONS=${ITERATIONS:-10}
|
||||
LOGDIR=${LOGDIR:-/data/vllm-ascend-smoke/logs/ttft-$(date +%Y%m%d-%H%M%S)}
|
||||
mkdir -p "$LOGDIR"
|
||||
echo "$LOGDIR" > /data/vllm-ascend-smoke/logs/latest-ttft-dir.txt
|
||||
|
||||
python /data/vllm-ascend-smoke/ttft_compare.py \
|
||||
--model "$MODEL" \
|
||||
--iterations "$ITERATIONS" \
|
||||
--case baseline_0_1:0:1:noop \
|
||||
--case prefetch_4_1:4:1:prefetch \
|
||||
--case sync_4_1:4:1:sync \
|
||||
2>&1 | tee "$LOGDIR/ttft-compare.log"
|
||||
|
||||
grep -H "TTFT_RESULT" "$LOGDIR"/ttft-compare.log
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import json
|
||||
import subprocess
|
||||
import time
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
def npu_mem():
|
||||
try:
|
||||
out = subprocess.check_output(['npu-smi', 'info'], text=True, stderr=subprocess.STDOUT)
|
||||
return [line for line in out.splitlines() if '910B' in line or 'NPU' in line or 'Memory-Usage' in line][:20]
|
||||
except Exception as e:
|
||||
return [f'npu-smi failed: {e}']
|
||||
|
||||
print('MEM_BEFORE', json.dumps(npu_mem(), ensure_ascii=False), flush=True)
|
||||
start = time.perf_counter()
|
||||
llm = LLM(
|
||||
model='/data/vllm-ascend-smoke/models/Qwen/Qwen2.5-0.5B-Instruct',
|
||||
trust_remote_code=True,
|
||||
dtype='float16',
|
||||
max_model_len=256,
|
||||
max_num_seqs=1,
|
||||
max_num_batched_tokens=256,
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
gpu_memory_utilization=0.6,
|
||||
offload_group_size=4,
|
||||
offload_num_in_group=1,
|
||||
)
|
||||
print('LOAD_SECONDS', round(time.perf_counter() - start, 4), flush=True)
|
||||
params = SamplingParams(temperature=0.0, max_tokens=8)
|
||||
latencies = []
|
||||
for i in range(100):
|
||||
t = time.perf_counter()
|
||||
out = llm.generate([f'Number {i}: say hello in five words.'], params, use_tqdm=False)
|
||||
latencies.append(time.perf_counter() - t)
|
||||
if not out or not out[0].outputs or len(out[0].outputs[0].token_ids) == 0:
|
||||
raise RuntimeError(f'empty output at {i}')
|
||||
print('STABILITY_RESULT', json.dumps({
|
||||
'requests': 100,
|
||||
'min_s': round(min(latencies), 4),
|
||||
'max_s': round(max(latencies), 4),
|
||||
'avg_s': round(sum(latencies)/len(latencies), 4),
|
||||
}, sort_keys=True), flush=True)
|
||||
print('MEM_AFTER', json.dumps(npu_mem(), ensure_ascii=False), flush=True)
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
import argparse
|
||||
import gc
|
||||
import json
|
||||
import os
|
||||
import statistics
|
||||
import time
|
||||
|
||||
from vllm import LLM, SamplingParams
|
||||
|
||||
|
||||
def parse_case(raw: str) -> tuple[str, int, int, str]:
|
||||
parts = raw.split(":")
|
||||
if len(parts) not in (3, 4):
|
||||
raise argparse.ArgumentTypeError(
|
||||
"case must be LABEL:OFFLOAD_GROUP_SIZE:OFFLOAD_NUM_IN_GROUP[:IMPL]"
|
||||
)
|
||||
label, group_size, num_in_group = parts[:3]
|
||||
impl = parts[3] if len(parts) == 4 else "prefetch"
|
||||
if impl not in {"prefetch", "sync", "noop"}:
|
||||
raise argparse.ArgumentTypeError("impl must be one of: prefetch, sync, noop")
|
||||
return label, int(group_size), int(num_in_group), impl
|
||||
|
||||
|
||||
def configure_impl(impl: str) -> None:
|
||||
if impl == "sync":
|
||||
os.environ["ASCEND_OFFLOAD_IMPL"] = "sync"
|
||||
else:
|
||||
os.environ.pop("ASCEND_OFFLOAD_IMPL", None)
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--model", required=True)
|
||||
parser.add_argument("--case", action="append", type=parse_case, required=True)
|
||||
parser.add_argument("--iterations", type=int, default=10)
|
||||
parser.add_argument("--max-model-len", type=int, default=256)
|
||||
parser.add_argument("--gpu-memory-utilization", type=float, default=0.6)
|
||||
args = parser.parse_args()
|
||||
|
||||
prompts = [
|
||||
"Briefly define vLLM Ascend.",
|
||||
"Say one short sentence about CPU weight offload.",
|
||||
"Name one benefit of reducing NPU memory usage.",
|
||||
]
|
||||
|
||||
for label, group_size, num_in_group, impl in args.case:
|
||||
configure_impl(impl)
|
||||
load_start = time.perf_counter()
|
||||
llm = LLM(
|
||||
model=args.model,
|
||||
trust_remote_code=True,
|
||||
dtype="float16",
|
||||
max_model_len=args.max_model_len,
|
||||
max_num_seqs=1,
|
||||
max_num_batched_tokens=args.max_model_len,
|
||||
tensor_parallel_size=1,
|
||||
enforce_eager=True,
|
||||
gpu_memory_utilization=args.gpu_memory_utilization,
|
||||
offload_group_size=group_size,
|
||||
offload_num_in_group=num_in_group,
|
||||
)
|
||||
load_seconds = time.perf_counter() - load_start
|
||||
|
||||
sampling = SamplingParams(temperature=0.0, max_tokens=1)
|
||||
llm.generate(["warm up"], sampling, use_tqdm=False)
|
||||
|
||||
latencies = []
|
||||
for i in range(args.iterations):
|
||||
prompt = prompts[i % len(prompts)]
|
||||
start = time.perf_counter()
|
||||
outputs = llm.generate([prompt], sampling, use_tqdm=False)
|
||||
elapsed = time.perf_counter() - start
|
||||
if not outputs or not outputs[0].outputs or not outputs[0].outputs[0].token_ids:
|
||||
raise RuntimeError(f"empty first-token output for case {label}, iter {i}")
|
||||
latencies.append(elapsed)
|
||||
|
||||
result = {
|
||||
"label": label,
|
||||
"impl": impl,
|
||||
"model": args.model,
|
||||
"offload_group_size": group_size,
|
||||
"offload_num_in_group": num_in_group,
|
||||
"iterations": args.iterations,
|
||||
"load_seconds": round(load_seconds, 4),
|
||||
"ttft_proxy_avg_s": round(statistics.mean(latencies), 4),
|
||||
"ttft_proxy_min_s": round(min(latencies), 4),
|
||||
"ttft_proxy_max_s": round(max(latencies), 4),
|
||||
"ttft_proxy_p50_s": round(statistics.median(latencies), 4),
|
||||
}
|
||||
print("TTFT_RESULT", json.dumps(result, sort_keys=True), flush=True)
|
||||
|
||||
del llm
|
||||
gc.collect()
|
||||
|
|
@ -0,0 +1,384 @@
|
|||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import csv
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
CASES = {
|
||||
"baseline_0_1": {"group_size": 0, "num_in_group": 1},
|
||||
"group_8_num_1": {"group_size": 8, "num_in_group": 1},
|
||||
"group_8_num_2": {"group_size": 8, "num_in_group": 2},
|
||||
}
|
||||
DATASETS = ("gsm8k", "gpqa")
|
||||
FORMAL_SEED = "0"
|
||||
FORMAL_CONTRACTS = {
|
||||
"gsm8k": {
|
||||
"total": 1319,
|
||||
"dataset_sha256": (
|
||||
"32c548f08195e19e33408b844dd7be6aa4bcae457d957bc05909ff4bd4a00595"
|
||||
),
|
||||
"prompt_protocol": {
|
||||
"name": "qwen-chat-v1",
|
||||
"chat_template": True,
|
||||
"answer_format": "final_numeric",
|
||||
},
|
||||
"sampling_settings": {
|
||||
"temperature": 0,
|
||||
"max_tokens": 512,
|
||||
"enforce_eager": True,
|
||||
},
|
||||
},
|
||||
"gpqa": {
|
||||
"total": 198,
|
||||
"dataset_sha256": (
|
||||
"0c16aaa98af019ca691f98eeec9f99ca4cabfb9c920c9e19e40ebedf8d6a0174"
|
||||
),
|
||||
"prompt_protocol": {
|
||||
"name": "qwen-chat-v1",
|
||||
"chat_template": True,
|
||||
"answer_format": "leading_choice",
|
||||
},
|
||||
"sampling_settings": {
|
||||
"temperature": 0,
|
||||
"max_tokens": 8,
|
||||
"enforce_eager": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_result(results_dir, case, dataset):
|
||||
path = results_dir / case / f"{dataset}-result.json"
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
payload = json.loads(raw)
|
||||
except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
|
||||
raise RuntimeError(f"unable to read accuracy result: {path}") from error
|
||||
if not isinstance(payload, dict):
|
||||
raise RuntimeError(f"accuracy result must be an object: {path}")
|
||||
return payload, hashlib.sha256(raw).hexdigest()
|
||||
|
||||
|
||||
def load_examples_evidence(results_dir, case, dataset):
|
||||
path = results_dir / case / f"{dataset}-examples.jsonl"
|
||||
try:
|
||||
raw = path.read_bytes()
|
||||
text = raw.decode("utf-8")
|
||||
except (OSError, UnicodeDecodeError) as error:
|
||||
raise RuntimeError(f"unable to read accuracy examples: {path}") from error
|
||||
|
||||
total = 0
|
||||
correct = 0
|
||||
lines = text.split("\n")
|
||||
if lines[-1:] == [""]:
|
||||
lines.pop()
|
||||
# JSONL uses LF delimiters; str.splitlines() also splits valid JSON on U+2028.
|
||||
for line_number, line in enumerate(lines, 1):
|
||||
if not line.strip():
|
||||
raise RuntimeError(f"invalid accuracy examples: {path}:{line_number}")
|
||||
try:
|
||||
example = json.loads(line)
|
||||
except json.JSONDecodeError as error:
|
||||
raise RuntimeError(
|
||||
f"invalid accuracy examples: {path}:{line_number}"
|
||||
) from error
|
||||
if not isinstance(example, dict) or not isinstance(
|
||||
example.get("correct"), bool
|
||||
):
|
||||
raise RuntimeError(f"invalid accuracy examples: {path}:{line_number}")
|
||||
total += 1
|
||||
correct += int(example["correct"])
|
||||
return {
|
||||
"examples_sha256": hashlib.sha256(raw).hexdigest(),
|
||||
"evidence_total": total,
|
||||
"evidence_correct": correct,
|
||||
}
|
||||
|
||||
|
||||
def validate_result(
|
||||
payload, case, dataset, expected_settings, expected_backend
|
||||
):
|
||||
if payload.get("dataset") != dataset:
|
||||
raise RuntimeError(f"dataset metadata mismatch for {case}/{dataset}")
|
||||
offload_settings = payload.get("offload_settings")
|
||||
if (
|
||||
not isinstance(offload_settings, dict)
|
||||
or offload_settings.get("group_size") != expected_settings["group_size"]
|
||||
or offload_settings.get("num_in_group")
|
||||
!= expected_settings["num_in_group"]
|
||||
):
|
||||
raise RuntimeError(f"offload settings mismatch for {case}/{dataset}")
|
||||
if (
|
||||
"backend" not in offload_settings
|
||||
or offload_settings["backend"] != expected_backend
|
||||
):
|
||||
raise RuntimeError(f"offload backend mismatch for {case}/{dataset}")
|
||||
prompt_protocol = payload.get("prompt_protocol")
|
||||
if (
|
||||
not isinstance(prompt_protocol, dict)
|
||||
or not isinstance(prompt_protocol.get("name"), str)
|
||||
or not prompt_protocol["name"]
|
||||
):
|
||||
raise RuntimeError(f"missing prompt protocol for {case}/{dataset}")
|
||||
sampling_settings = payload.get("sampling_settings")
|
||||
if not isinstance(sampling_settings, dict):
|
||||
raise RuntimeError(f"missing sampling settings for {case}/{dataset}")
|
||||
if not isinstance(payload.get("model"), str) or not payload["model"].strip():
|
||||
raise RuntimeError(f"missing model metadata for {case}/{dataset}")
|
||||
if not isinstance(payload.get("seed"), str) or not payload["seed"]:
|
||||
raise RuntimeError(f"missing seed metadata for {case}/{dataset}")
|
||||
if not isinstance(payload.get("dataset_source"), dict):
|
||||
raise RuntimeError(f"missing dataset source for {case}/{dataset}")
|
||||
|
||||
total = payload.get("total")
|
||||
correct = payload.get("correct")
|
||||
accuracy = payload.get("accuracy")
|
||||
if (
|
||||
isinstance(total, bool)
|
||||
or not isinstance(total, int)
|
||||
or total <= 0
|
||||
or isinstance(correct, bool)
|
||||
or not isinstance(correct, int)
|
||||
or correct < 0
|
||||
or correct > total
|
||||
or not isinstance(accuracy, (int, float))
|
||||
):
|
||||
raise RuntimeError(f"invalid score metadata for {case}/{dataset}")
|
||||
if abs(float(accuracy) - correct / total) > 1e-12:
|
||||
raise RuntimeError(f"accuracy does not match correct/total for {case}/{dataset}")
|
||||
if not payload.get("dataset_sha256"):
|
||||
raise RuntimeError(f"missing dataset hash for {case}/{dataset}")
|
||||
|
||||
|
||||
def validate_formal_contract(payload, case, dataset, allow_limited):
|
||||
contract = FORMAL_CONTRACTS[dataset]
|
||||
if payload["dataset_sha256"] != contract["dataset_sha256"]:
|
||||
raise RuntimeError(f"formal dataset hash mismatch for {case}/{dataset}")
|
||||
if payload["prompt_protocol"] != contract["prompt_protocol"]:
|
||||
raise RuntimeError(f"formal prompt protocol mismatch for {case}/{dataset}")
|
||||
if payload["sampling_settings"] != contract["sampling_settings"]:
|
||||
raise RuntimeError(f"formal sampling settings mismatch for {case}/{dataset}")
|
||||
if payload["seed"] != FORMAL_SEED:
|
||||
raise RuntimeError(f"formal seed mismatch for {case}/{dataset}")
|
||||
|
||||
total = payload["total"]
|
||||
expected_total = contract["total"]
|
||||
valid_total = total <= expected_total if allow_limited else total == expected_total
|
||||
if not valid_total:
|
||||
mode = "limited" if allow_limited else "formal"
|
||||
raise RuntimeError(
|
||||
f"formal sample count mismatch for {case}/{dataset}: "
|
||||
f"{total} is invalid in {mode} mode"
|
||||
)
|
||||
|
||||
|
||||
def verify_matrix(
|
||||
results_dir,
|
||||
max_accuracy_drop,
|
||||
allow_limited=False,
|
||||
expected_positive_backend=None,
|
||||
):
|
||||
if max_accuracy_drop < 0:
|
||||
raise ValueError("max_accuracy_drop must be non-negative")
|
||||
results_dir = Path(results_dir)
|
||||
rows = []
|
||||
matrix_model = None
|
||||
dataset_sources = {}
|
||||
|
||||
for dataset in DATASETS:
|
||||
baseline, baseline_result_sha256 = load_result(
|
||||
results_dir, "baseline_0_1", dataset
|
||||
)
|
||||
validate_result(
|
||||
baseline,
|
||||
"baseline_0_1",
|
||||
dataset,
|
||||
CASES["baseline_0_1"],
|
||||
None,
|
||||
)
|
||||
validate_formal_contract(
|
||||
baseline, "baseline_0_1", dataset, allow_limited
|
||||
)
|
||||
baseline_accuracy = float(baseline["accuracy"])
|
||||
baseline_hash = baseline["dataset_sha256"]
|
||||
baseline_total = baseline["total"]
|
||||
baseline_protocol = baseline["prompt_protocol"]
|
||||
baseline_sampling = baseline["sampling_settings"]
|
||||
baseline_seed = baseline["seed"]
|
||||
baseline_model = baseline["model"]
|
||||
baseline_source = baseline["dataset_source"]
|
||||
dataset_sources[dataset] = baseline_source
|
||||
if matrix_model is None:
|
||||
matrix_model = baseline_model
|
||||
elif baseline_model != matrix_model:
|
||||
raise RuntimeError(f"model mismatch for baseline_0_1/{dataset}")
|
||||
|
||||
for case, expected_settings in CASES.items():
|
||||
if case == "baseline_0_1":
|
||||
result = baseline
|
||||
result_sha256 = baseline_result_sha256
|
||||
else:
|
||||
result, result_sha256 = load_result(results_dir, case, dataset)
|
||||
expected_backend = (
|
||||
None if case == "baseline_0_1" else expected_positive_backend
|
||||
)
|
||||
validate_result(
|
||||
result, case, dataset, expected_settings, expected_backend
|
||||
)
|
||||
validate_formal_contract(result, case, dataset, allow_limited)
|
||||
if result["dataset_sha256"] != baseline_hash:
|
||||
raise RuntimeError(f"dataset hash mismatch for {case}/{dataset}")
|
||||
if result["total"] != baseline_total:
|
||||
raise RuntimeError(f"sample count mismatch for {case}/{dataset}")
|
||||
if result["prompt_protocol"] != baseline_protocol:
|
||||
raise RuntimeError(f"prompt protocol mismatch for {case}/{dataset}")
|
||||
if result["sampling_settings"] != baseline_sampling:
|
||||
raise RuntimeError(f"sampling settings mismatch for {case}/{dataset}")
|
||||
if result["seed"] != baseline_seed:
|
||||
raise RuntimeError(f"seed mismatch for {case}/{dataset}")
|
||||
if result["model"] != baseline_model:
|
||||
raise RuntimeError(f"model mismatch for {case}/{dataset}")
|
||||
if result["dataset_source"] != baseline_source:
|
||||
raise RuntimeError(f"dataset source mismatch for {case}/{dataset}")
|
||||
|
||||
evidence = load_examples_evidence(results_dir, case, dataset)
|
||||
if (
|
||||
evidence["evidence_total"] != result["total"]
|
||||
or evidence["evidence_correct"] != result["correct"]
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"examples evidence mismatch for {case}/{dataset}: "
|
||||
f"result={result['correct']}/{result['total']} "
|
||||
f"examples={evidence['evidence_correct']}/"
|
||||
f"{evidence['evidence_total']}"
|
||||
)
|
||||
|
||||
accuracy = float(result["accuracy"])
|
||||
change = accuracy - baseline_accuracy
|
||||
passed = change >= -max_accuracy_drop - 1e-12
|
||||
rows.append(
|
||||
{
|
||||
"case": case,
|
||||
"dataset": dataset,
|
||||
"correct": result["correct"],
|
||||
"total": result["total"],
|
||||
"accuracy": accuracy,
|
||||
"baseline_accuracy": baseline_accuracy,
|
||||
"accuracy_change": change,
|
||||
"passed": passed,
|
||||
"dataset_sha256": result["dataset_sha256"],
|
||||
"actual_backend": result["offload_settings"]["backend"],
|
||||
"expected_backend": expected_backend,
|
||||
"result_sha256": result_sha256,
|
||||
**evidence,
|
||||
}
|
||||
)
|
||||
if not passed:
|
||||
raise RuntimeError(
|
||||
f"accuracy drop for {case}/{dataset}: "
|
||||
f"{change:.12f} < -{max_accuracy_drop:.12f}"
|
||||
)
|
||||
|
||||
return {
|
||||
"baseline_case": "baseline_0_1",
|
||||
"expected_positive_backend": expected_positive_backend,
|
||||
"max_accuracy_drop": max_accuracy_drop,
|
||||
"verification_mode": "limited" if allow_limited else "formal",
|
||||
"model": matrix_model,
|
||||
"seed": FORMAL_SEED,
|
||||
"dataset_sources": dataset_sources,
|
||||
"formal_contracts": FORMAL_CONTRACTS,
|
||||
"all_passed": all(row["passed"] for row in rows),
|
||||
"rows": rows,
|
||||
}
|
||||
|
||||
|
||||
def write_summaries(summary, output_dir):
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
json_path = output_dir / "accuracy-summary.json"
|
||||
csv_path = output_dir / "accuracy-summary.csv"
|
||||
markdown_path = output_dir / "accuracy-summary.md"
|
||||
|
||||
json_path.write_text(
|
||||
json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
||||
)
|
||||
fieldnames = list(summary["rows"][0])
|
||||
with csv_path.open("w", encoding="utf-8", newline="") as file:
|
||||
writer = csv.DictWriter(file, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(summary["rows"])
|
||||
|
||||
lines = [
|
||||
"# Accuracy Matrix",
|
||||
"",
|
||||
f"Verification mode: `{summary['verification_mode']}`",
|
||||
"",
|
||||
"Expected positive backend: "
|
||||
f"`{summary['expected_positive_backend'] or 'none'}`",
|
||||
"",
|
||||
f"Model: `{summary['model']}`",
|
||||
"",
|
||||
f"Seed: `{summary['seed']}`",
|
||||
"",
|
||||
f"Maximum allowed accuracy drop: `{summary['max_accuracy_drop']}`",
|
||||
"",
|
||||
"Evidence shows recomputed correct/total and result/examples SHA256 "
|
||||
"prefixes.",
|
||||
"",
|
||||
"| Case | Dataset | Backend | Expected backend | Correct | Total | "
|
||||
"Accuracy | Change | Evidence | Passed |",
|
||||
"| --- | --- | --- | --- | ---: | ---: | ---: | ---: | --- | --- |",
|
||||
]
|
||||
for row in summary["rows"]:
|
||||
actual_backend = row["actual_backend"] or "none"
|
||||
expected_backend = row["expected_backend"] or "none"
|
||||
evidence = (
|
||||
f"{row['evidence_correct']}/{row['evidence_total']}; "
|
||||
f"{row['result_sha256'][:12]} / {row['examples_sha256'][:12]}"
|
||||
)
|
||||
lines.append(
|
||||
f"| {row['case']} | {row['dataset']} | {actual_backend} | "
|
||||
f"{expected_backend} | {row['correct']} | {row['total']} | "
|
||||
f"{row['accuracy']:.6f} | {row['accuracy_change']:+.6f} | "
|
||||
f"{evidence} | {row['passed']} |"
|
||||
)
|
||||
markdown_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
return json_path, csv_path, markdown_path
|
||||
|
||||
|
||||
def parse_args(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--results-dir", type=Path, required=True)
|
||||
parser.add_argument("--max-accuracy-drop", type=float, default=0.0)
|
||||
parser.add_argument(
|
||||
"--expected-positive-backend",
|
||||
choices=("auto", "prefetch", "uva"),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--allow-limited",
|
||||
action="store_true",
|
||||
help="allow a positive sample count below the formal dataset total",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
if args.max_accuracy_drop < 0:
|
||||
parser.error("--max-accuracy-drop must be non-negative")
|
||||
return args
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
args = parse_args(argv)
|
||||
summary = verify_matrix(
|
||||
args.results_dir,
|
||||
args.max_accuracy_drop,
|
||||
allow_limited=args.allow_limited,
|
||||
expected_positive_backend=args.expected_positive_backend,
|
||||
)
|
||||
paths = write_summaries(summary, args.results_dir)
|
||||
print(json.dumps({"all_passed": True, "artifacts": [str(path) for path in paths]}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,443 @@
|
|||
import importlib.util
|
||||
import re
|
||||
import sys
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
from unittest.mock import patch
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root():
|
||||
for parent in Path(__file__).resolve().parents:
|
||||
if (parent / "scripts" / "bench_one.py").is_file():
|
||||
return parent
|
||||
raise RuntimeError("unable to locate repository scripts directory")
|
||||
|
||||
|
||||
ROOT = find_repo_root()
|
||||
|
||||
|
||||
def load_script(name: str):
|
||||
path = ROOT / "scripts" / name
|
||||
spec = importlib.util.spec_from_file_location(path.stem, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class BenchmarkUnitTests(unittest.TestCase):
|
||||
def test_offload_backend_is_optional_in_llm_kwargs_and_benchmark_metadata(self):
|
||||
bench = load_script("bench_one.py")
|
||||
base_args = {
|
||||
"model": "test-model",
|
||||
"max_model_len": 512,
|
||||
"prompt_count": 8,
|
||||
"gpu_memory_utilization": 0.5,
|
||||
"seed": 0,
|
||||
"offload_group_size": 8,
|
||||
"offload_num_in_group": 2,
|
||||
}
|
||||
|
||||
omitted = Namespace(**base_args, offload_backend=None)
|
||||
explicit = Namespace(**base_args, offload_backend="prefetch")
|
||||
|
||||
self.assertEqual(
|
||||
bench.build_llm_kwargs(omitted),
|
||||
{
|
||||
"model": "test-model",
|
||||
"trust_remote_code": True,
|
||||
"dtype": "float16",
|
||||
"max_model_len": 512,
|
||||
"max_num_seqs": 8,
|
||||
"max_num_batched_tokens": 4096,
|
||||
"tensor_parallel_size": 1,
|
||||
"enforce_eager": True,
|
||||
"gpu_memory_utilization": 0.5,
|
||||
"seed": 0,
|
||||
"offload_group_size": 8,
|
||||
"offload_num_in_group": 2,
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
bench.build_llm_kwargs(explicit)["offload_backend"], "prefetch"
|
||||
)
|
||||
self.assertEqual(bench.offload_result_metadata(omitted), {"offload_backend": None})
|
||||
self.assertEqual(
|
||||
bench.offload_result_metadata(explicit), {"offload_backend": "prefetch"}
|
||||
)
|
||||
|
||||
def test_benchmark_cli_accepts_the_explicit_offload_backend_choices(self):
|
||||
bench = load_script("bench_one.py")
|
||||
with patch.object(
|
||||
sys,
|
||||
"argv",
|
||||
[
|
||||
"bench_one.py",
|
||||
"--model", "test-model",
|
||||
"--label", "group_8_num_1",
|
||||
"--offload-group-size", "8",
|
||||
"--offload-num-in-group", "1",
|
||||
"--result-file", "result.jsonl",
|
||||
"--offload-backend", "prefetch",
|
||||
],
|
||||
):
|
||||
self.assertEqual(bench.parse_args().offload_backend, "prefetch")
|
||||
|
||||
def test_detect_npu_index_handles_reallocated_physical_card(self):
|
||||
detector = load_script("detect_npu_index.py")
|
||||
old_mapping = """
|
||||
NPU ID Chip ID Chip Logic ID Chip Name
|
||||
7 0 0 Ascend 910B2C
|
||||
7 1 - Mcu
|
||||
"""
|
||||
new_mapping = """
|
||||
NPU ID Chip ID Chip Logic ID Chip Name
|
||||
0 0 0 Ascend 910B2C
|
||||
0 1 - Mcu
|
||||
"""
|
||||
self.assertEqual(detector.parse_mapping(old_mapping), 7)
|
||||
self.assertEqual(detector.parse_mapping(new_mapping), 0)
|
||||
with self.assertRaisesRegex(RuntimeError, "Ascend NPU mapping"):
|
||||
detector.parse_mapping("NPU ID Chip ID\n")
|
||||
|
||||
def test_parse_npu_smi_hbm_tracks_device_across_target_machine_rows(self):
|
||||
bench = load_script("bench_one.py")
|
||||
output = """
|
||||
+--------------------------------------------------------------------------------------------------+
|
||||
| 7 910B2C | Alarm | 70.1 42 0 0 / 0 |
|
||||
| 0 | 0000:CF:00.0 | 0 0 / 0 3413 / 65536 |
|
||||
| 6 910B2C | OK | 68.2 40 0 0 / 0 |
|
||||
| 0 | 0000:AF:00.0 | 0 0 / 0 9876 / 65536 |
|
||||
+--------------------------------------------------------------------------------------------------+
|
||||
| NPU | Chip | Process id | Process name | Process memory(MB) |
|
||||
| 7 | 0 | 65536 | python | 9999 |
|
||||
"""
|
||||
self.assertEqual(
|
||||
bench.parse_npu_smi_hbm(output, npu_index=7),
|
||||
{"used_mib": 3413, "total_mib": 65536},
|
||||
)
|
||||
self.assertIsNone(bench.parse_npu_smi_hbm("unexpected output", 7))
|
||||
|
||||
def test_parse_npu_smi_hbm_does_not_cross_device_or_process_rows(self):
|
||||
bench = load_script("bench_one.py")
|
||||
output = """
|
||||
| 7 910B2C | Alarm | 70.1 42 0 0 / 0 |
|
||||
| 0 | not-a-pci-address | 0 0 / 0 1111 / 65536 |
|
||||
| 6 910B2C | OK | 68.2 40 0 0 / 0 |
|
||||
| 0 | 0000:AF:00.0 | 0 0 / 0 9876 / 65536 |
|
||||
| NPU | Chip | Process id | Process name | Process memory(MB) |
|
||||
| 7 | 0 | 12345 | python | 7777 / 65536 |
|
||||
"""
|
||||
self.assertIsNone(bench.parse_npu_smi_hbm(output, npu_index=7))
|
||||
self.assertEqual(
|
||||
bench.parse_npu_smi_hbm(output, npu_index=6),
|
||||
{"used_mib": 9876, "total_mib": 65536},
|
||||
)
|
||||
|
||||
def test_hbm_summary_does_not_replace_failed_before_sample(self):
|
||||
bench = load_script("bench_one.py")
|
||||
sampler = bench.ResourceSampler(7)
|
||||
sampler.samples = [
|
||||
{"used_mib": None, "total_mib": None, "raw": "parse failed"},
|
||||
{"used_mib": 1024, "total_mib": 65536},
|
||||
{"used_mib": 2048, "total_mib": 65536},
|
||||
]
|
||||
self.assertEqual(sampler.hbm_summary(), (None, 2048, 2048))
|
||||
|
||||
def test_resource_sampling_uses_phase_boundaries_not_background_npu_smi(self):
|
||||
script = self.read_benchmark_source()
|
||||
self.assertNotIn("threading.Thread", script)
|
||||
self.assertNotIn("while not self._stop", script)
|
||||
self.assertEqual(script.count("sampler.checkpoint("), 2)
|
||||
self.assertIn('sampler.checkpoint("before_load")', script)
|
||||
self.assertIn('sampler.checkpoint("after_generation")', script)
|
||||
|
||||
@staticmethod
|
||||
def read_benchmark_source():
|
||||
return (ROOT / "scripts" / "bench_one.py").read_text(encoding="utf-8")
|
||||
|
||||
def test_comparison_formulas_handle_zero_missing_and_negative_changes(self):
|
||||
summary = load_script("bench_param_matrix.py")
|
||||
self.assertIsNone(summary.percent_change(12.0, 0.0))
|
||||
self.assertIsNone(summary.percent_change(None, 10.0))
|
||||
self.assertEqual(summary.percent_change(8.0, 10.0), -20.0)
|
||||
self.assertIsNone(summary.hbm_saved_gib(None, 1024.0))
|
||||
self.assertEqual(summary.hbm_saved_gib(4096.0, 1024.0), 3.0)
|
||||
|
||||
def test_aggregation_groups_labels_and_uses_median(self):
|
||||
summary = load_script("bench_param_matrix.py")
|
||||
records = [
|
||||
{"label": "baseline_0_1", "output_tokens_per_second": 10.0,
|
||||
"npu_hbm_peak_mib": 4096.0},
|
||||
{"label": "group_8_num_1", "output_tokens_per_second": 5.0,
|
||||
"npu_hbm_peak_mib": 2048.0},
|
||||
{"label": "baseline_0_1", "output_tokens_per_second": 30.0,
|
||||
"npu_hbm_peak_mib": 6144.0},
|
||||
{"label": "group_8_num_1", "output_tokens_per_second": 25.0,
|
||||
"npu_hbm_peak_mib": 3072.0},
|
||||
{"label": "baseline_0_1", "output_tokens_per_second": 20.0,
|
||||
"npu_hbm_peak_mib": 5120.0},
|
||||
{"label": "group_8_num_1", "output_tokens_per_second": 15.0,
|
||||
"npu_hbm_peak_mib": 1024.0},
|
||||
]
|
||||
rows = summary.aggregate_records(records, "baseline_0_1")
|
||||
by_label = {row["label"]: row for row in rows}
|
||||
self.assertEqual(by_label["baseline_0_1"]["repeats"], 3)
|
||||
self.assertEqual(
|
||||
by_label["baseline_0_1"]["output_tokens_per_second"], 20.0
|
||||
)
|
||||
self.assertEqual(
|
||||
by_label["group_8_num_1"]["npu_hbm_peak_mib"], 2048.0
|
||||
)
|
||||
self.assertEqual(
|
||||
by_label["group_8_num_1"]["throughput_change_percent"], -25.0
|
||||
)
|
||||
self.assertEqual(by_label["group_8_num_1"]["hbm_saved_gib"], 3.0)
|
||||
|
||||
|
||||
class ScriptAcceptanceTests(unittest.TestCase):
|
||||
def read(self, name: str) -> str:
|
||||
return (ROOT / "scripts" / name).read_text(encoding="utf-8")
|
||||
|
||||
def test_scripts_are_portable_and_have_no_old_smoke_path(self):
|
||||
names = (
|
||||
"bench_one.py",
|
||||
"bench_param_matrix.py",
|
||||
"run_benchmark_all.sh",
|
||||
"run_reproduce_all.sh",
|
||||
)
|
||||
for name in names:
|
||||
with self.subTest(name=name):
|
||||
text = self.read(name)
|
||||
self.assertNotIn("/data/vllm-ascend-smoke", text)
|
||||
benchmark = self.read("run_benchmark_all.sh")
|
||||
self.assertIn('BASH_SOURCE[0]', benchmark)
|
||||
self.assertIn(
|
||||
"CANN_ENV=${CANN_ENV:-"
|
||||
"/usr/local/Ascend/cann-8.5.1/cann-8.5.1/set_env.sh}",
|
||||
benchmark,
|
||||
)
|
||||
self.assertIn(
|
||||
"VENV_ACTIVATE=${VENV_ACTIVATE:-"
|
||||
"/data/vllm-ascend-v0.18.0/.venv/bin/activate}",
|
||||
benchmark,
|
||||
)
|
||||
self.assertIn('source "${CANN_ENV}"', benchmark)
|
||||
self.assertIn('source "${VENV_ACTIVATE}"', benchmark)
|
||||
|
||||
def test_runtime_environment_paths_can_be_overridden_or_disabled(self):
|
||||
for name in (
|
||||
"run_benchmark_all.sh",
|
||||
"run_accuracy_gsm8k_gpqa.sh",
|
||||
"run_reproduce_all.sh",
|
||||
):
|
||||
with self.subTest(name=name):
|
||||
script = self.read(name)
|
||||
self.assertIn("CANN_ENV=${CANN_ENV:-", script)
|
||||
self.assertIn("VENV_ACTIVATE=${VENV_ACTIVATE:-", script)
|
||||
self.assertIn('source "${CANN_ENV}"', script)
|
||||
self.assertIn('source "${VENV_ACTIVATE}"', script)
|
||||
self.assertIn(
|
||||
"VLLM_ASCEND_CUSTOM_OP_ROOT="
|
||||
"${VLLM_ASCEND_CUSTOM_OP_ROOT-",
|
||||
script,
|
||||
)
|
||||
self.assertIn(
|
||||
'if [[ -n "${VLLM_ASCEND_CUSTOM_OP_ROOT}" ]]', script
|
||||
)
|
||||
|
||||
def test_benchmark_has_three_cases_rotation_and_fresh_processes(self):
|
||||
script = self.read("run_benchmark_all.sh")
|
||||
for case in ("baseline_0_1:0:1", "group_8_num_1:8:1", "group_8_num_2:8:2"):
|
||||
self.assertIn(case, script)
|
||||
for variable in (
|
||||
"MODEL_PATH", "RESULTS_DIR", "REPEATS", "WARMUP_ROUNDS",
|
||||
"MEASURED_ROUNDS", "PROMPT_COUNT", "MAX_MODEL_LEN",
|
||||
"MAX_TOKENS", "GPU_MEMORY_UTILIZATION", "SEED",
|
||||
"ASCEND_RT_VISIBLE_DEVICES",
|
||||
):
|
||||
self.assertIn(variable, script)
|
||||
strict_mode = script.index("set -euo pipefail")
|
||||
repeat_loop = script.index("for ((repeat=0; repeat<REPEATS; repeat++)); do")
|
||||
offset_loop = script.index(
|
||||
"for ((offset=0; offset<${#CASES[@]}; offset++)); do"
|
||||
)
|
||||
python_case = script.index('python "${SCRIPT_DIR}/bench_one.py"')
|
||||
first_inner_done = script.index(" done", python_case)
|
||||
first_outer_done = script.index("done", first_inner_done + len(" done"))
|
||||
self.assertLess(strict_mode, python_case)
|
||||
self.assertLess(repeat_loop, offset_loop)
|
||||
self.assertLess(offset_loop, python_case)
|
||||
self.assertLess(python_case, first_inner_done)
|
||||
self.assertLess(first_inner_done, first_outer_done)
|
||||
self.assertIn("start=$((repeat % ${#CASES[@]}))", script)
|
||||
self.assertIn("index=$(((start + offset) % ${#CASES[@]}))", script)
|
||||
self.assertIn(
|
||||
'IFS=: read -r label group_size num_in_group <<< "${CASES[index]}"',
|
||||
script,
|
||||
)
|
||||
case_pipeline = re.search(
|
||||
r'python "\$\{SCRIPT_DIR\}/bench_one\.py" \\\n'
|
||||
r'(?P<arguments>(?:\s+--[^\n]+\\\n)+)'
|
||||
r'\s+--npu-index "\$\{NPU_INDEX\}" 2>&1 \| tee -a '
|
||||
r'"\$\{RUN_LOG\}"',
|
||||
script,
|
||||
)
|
||||
self.assertIsNotNone(case_pipeline)
|
||||
arguments = case_pipeline.group("arguments")
|
||||
self.assertIn('--label "${label}"', arguments)
|
||||
self.assertIn('--offload-group-size "${group_size}"', arguments)
|
||||
self.assertIn('--offload-num-in-group "${num_in_group}"', arguments)
|
||||
self.assertIn(
|
||||
'--gpu-memory-utilization "${GPU_MEMORY_UTILIZATION}"', arguments
|
||||
)
|
||||
self.assertNotIn("|| true", case_pipeline.group(0))
|
||||
self.assertIn("set -euo pipefail", script[:python_case])
|
||||
self.assertNotIn("|| true", script)
|
||||
|
||||
def test_shell_entry_points_add_an_explicit_backend_only_for_positive_groups(self):
|
||||
benchmark = self.read("run_benchmark_all.sh")
|
||||
accuracy = self.read("run_accuracy_gsm8k_gpqa.sh")
|
||||
for script in (benchmark, accuracy):
|
||||
self.assertIn('OFFLOAD_BACKEND=${OFFLOAD_BACKEND:-}', script)
|
||||
self.assertIn('if [[ "$group_size" -gt 0 && -n "$OFFLOAD_BACKEND" ]]', script)
|
||||
self.assertIn('--offload-backend "$OFFLOAD_BACKEND"', script)
|
||||
|
||||
|
||||
def test_shell_paths_are_derived_from_bash_source(self):
|
||||
benchmark = self.read("run_benchmark_all.sh")
|
||||
reproduce = self.read("run_reproduce_all.sh")
|
||||
exact_script_dir = (
|
||||
'SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"'
|
||||
)
|
||||
self.assertIn(exact_script_dir, benchmark)
|
||||
self.assertIn(exact_script_dir, reproduce)
|
||||
self.assertIn(
|
||||
'REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"', reproduce
|
||||
)
|
||||
|
||||
def test_runtime_visible_device_and_npu_smi_index_are_independent(self):
|
||||
benchmark = self.read("run_benchmark_all.sh")
|
||||
reproduce = self.read("run_reproduce_all.sh")
|
||||
accuracy = self.read("run_accuracy_gsm8k_gpqa.sh")
|
||||
health = self.read("check_npu_health.sh")
|
||||
for name, script in (
|
||||
("run_benchmark_all.sh", benchmark),
|
||||
("run_reproduce_all.sh", reproduce),
|
||||
("run_accuracy_gsm8k_gpqa.sh", accuracy),
|
||||
("check_npu_health.sh", health),
|
||||
):
|
||||
with self.subTest(name=name):
|
||||
self.assertIn("detect_npu_index.py", script)
|
||||
self.assertNotIn("NPU_INDEX=${NPU_INDEX:-7}", script)
|
||||
for script in (benchmark, reproduce, accuracy):
|
||||
self.assertIn(
|
||||
"export ASCEND_RT_VISIBLE_DEVICES=${ASCEND_RT_VISIBLE_DEVICES:-0}",
|
||||
script,
|
||||
)
|
||||
self.assertIn('--npu-index "${NPU_INDEX}"', benchmark)
|
||||
|
||||
def test_runtime_scripts_export_bundled_custom_operator_paths(self):
|
||||
expected_root = (
|
||||
"/data/vllm-ascend-v0.18.0/vllm_ascend/"
|
||||
"_cann_ops_custom/vendors/vllm-ascend"
|
||||
)
|
||||
for name in (
|
||||
"run_benchmark_all.sh",
|
||||
"run_accuracy_gsm8k_gpqa.sh",
|
||||
"run_reproduce_all.sh",
|
||||
):
|
||||
with self.subTest(name=name):
|
||||
script = self.read(name)
|
||||
self.assertIn("VLLM_ASCEND_CUSTOM_OP_ROOT", script)
|
||||
self.assertIn(expected_root, script)
|
||||
self.assertIn("ASCEND_CUSTOM_OPP_PATH", script)
|
||||
self.assertIn("LD_LIBRARY_PATH", script)
|
||||
self.assertIn("TRITON_CACHE_DIR", script)
|
||||
|
||||
def test_runtime_scripts_fail_fast_on_unhealthy_npu(self):
|
||||
health_check = self.read("check_npu_health.sh")
|
||||
self.assertIn("npu-smi info -t health", health_check)
|
||||
self.assertIn("Health Status", health_check)
|
||||
self.assertIn("exit 42", health_check)
|
||||
for name in (
|
||||
"run_benchmark_all.sh",
|
||||
"run_accuracy_gsm8k_gpqa.sh",
|
||||
):
|
||||
with self.subTest(name=name):
|
||||
script = self.read(name)
|
||||
self.assertIn('bash "${SCRIPT_DIR}/check_npu_health.sh"', script)
|
||||
self.assertIn("NPU_CHIP_INDEX", script)
|
||||
|
||||
def test_bench_one_records_required_metrics_and_deterministic_settings(self):
|
||||
script = self.read("bench_one.py")
|
||||
for metric in (
|
||||
"load_seconds", "generate_seconds", "request_count", "output_tokens",
|
||||
"output_tokens_per_second", "requests_per_second", "round_latencies_seconds",
|
||||
"latency_p50_seconds", "latency_p95_seconds", "process_rss_before_mib",
|
||||
"process_rss_peak_mib", "process_rss_after_mib", "npu_hbm_before_mib",
|
||||
"npu_hbm_peak_mib", "npu_hbm_after_mib", "offload_group_size",
|
||||
"offload_num_in_group", "npu_hbm_samples",
|
||||
):
|
||||
self.assertIn(metric, script)
|
||||
self.assertIn("temperature=0", script)
|
||||
self.assertIn('"enforce_eager": True', script)
|
||||
|
||||
def test_unified_script_runs_performance_before_accuracy_and_writes_manifest(self):
|
||||
script = self.read("run_reproduce_all.sh")
|
||||
performance = script.index("run_benchmark_all.sh")
|
||||
accuracy = script.index("run_accuracy_gsm8k_gpqa.sh")
|
||||
manifest = script.index("reproduction-manifest.json")
|
||||
self.assertLess(performance, accuracy)
|
||||
self.assertLess(accuracy, manifest)
|
||||
for field in (
|
||||
"started_at", "finished_at", "commands", "package_versions",
|
||||
"source_commit", "model_path", "artifacts",
|
||||
):
|
||||
self.assertIn(field, script)
|
||||
self.assertIn("import shlex", script)
|
||||
self.assertIn(
|
||||
'repo_root / "scripts" / "run_benchmark_all.sh"', script
|
||||
)
|
||||
self.assertIn(
|
||||
'repo_root / "scripts" / "run_accuracy_gsm8k_gpqa.sh"', script
|
||||
)
|
||||
self.assertIn('f"RESULTS_DIR={performance_dir}"', script)
|
||||
self.assertIn('f"RESULTS_DIR={accuracy_dir}"', script)
|
||||
self.assertIn("shlex.join", script)
|
||||
self.assertNotIn("bash scripts/run_benchmark_all.sh", script)
|
||||
self.assertNotIn("bash scripts/run_accuracy_gsm8k_gpqa.sh", script)
|
||||
for artifact in (
|
||||
"accuracy_summary_json",
|
||||
"accuracy_summary_csv",
|
||||
"accuracy_summary_markdown",
|
||||
):
|
||||
self.assertIn(artifact, script)
|
||||
|
||||
def test_v018_runtime_compatibility_install_is_reproducible(self):
|
||||
script = self.read("install_v018_runtime_compat.sh")
|
||||
self.assertIn("set -euo pipefail", script)
|
||||
self.assertIn(
|
||||
"/usr/local/Ascend/cann-8.5.1/cann-8.5.1/set_env.sh", script
|
||||
)
|
||||
self.assertIn("/data/vllm-ascend-v0.18.0/.venv/bin/activate", script)
|
||||
self.assertIn("triton_ascend-3.2.0.dev20260322", script)
|
||||
self.assertIn("torch-npu==2.9.0", script)
|
||||
self.assertIn(
|
||||
"https://mirrors.huaweicloud.com/repository/pypi/simple", script
|
||||
)
|
||||
self.assertIn("vllm-ascend.obs.cn-north-4.myhuaweicloud.com", script)
|
||||
self.assertIn("--no-deps", script)
|
||||
self.assertIn('realpath "${REPO_ROOT}"', script)
|
||||
self.assertIn('pip install --no-build-isolation --no-deps -e', script)
|
||||
self.assertIn('version != "3.2.0.dev20260322"', script)
|
||||
self.assertIn('torch_npu_version != "2.9.0"', script)
|
||||
|
||||
def test_manifest_records_triton_ascend_runtime_version(self):
|
||||
script = self.read("run_reproduce_all.sh")
|
||||
self.assertIn('"triton-ascend"', script)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,42 @@
|
|||
import pytest
|
||||
|
||||
from vllm.model_executor.offloader.base import NoopOffloader
|
||||
from vllm_ascend.model_executor.offloader import AscendPrefetchOffloader
|
||||
from vllm_ascend.model_executor.offloader.selection import select_offload_layers
|
||||
from vllm_ascend.worker.model_runner_v1 import create_ascend_weight_offloader
|
||||
|
||||
from tests.model_executor.offloader.test_runner_integration import make_config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("group_size", "expected_layers"),
|
||||
[
|
||||
(0, ()),
|
||||
(1, tuple(range(8))),
|
||||
(2, (1, 3, 5, 7)),
|
||||
(4, (3, 7)),
|
||||
],
|
||||
)
|
||||
def test_offload_group_size_controls_layer_grouping(group_size, expected_layers):
|
||||
selection = select_offload_layers(
|
||||
num_layers=8,
|
||||
group_size=group_size,
|
||||
num_in_group=1,
|
||||
)
|
||||
|
||||
assert selection.layer_indices == expected_layers
|
||||
assert selection.enabled is bool(expected_layers)
|
||||
|
||||
|
||||
def test_offload_group_size_zero_keeps_default_noop_offloader():
|
||||
offloader = create_ascend_weight_offloader(make_config(0, 1))
|
||||
|
||||
assert isinstance(offloader, NoopOffloader)
|
||||
|
||||
|
||||
def test_positive_offload_group_size_enables_ascend_prefetch_offloader():
|
||||
offloader = create_ascend_weight_offloader(make_config(4, 1))
|
||||
|
||||
assert isinstance(offloader, AscendPrefetchOffloader)
|
||||
assert offloader.group_size == 4
|
||||
assert offloader.num_in_group == 1
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
import pytest
|
||||
|
||||
from vllm.model_executor.offloader.base import NoopOffloader
|
||||
from vllm_ascend.model_executor.offloader import AscendPrefetchOffloader
|
||||
from vllm_ascend.model_executor.offloader.selection import select_offload_layers
|
||||
from vllm_ascend.worker.model_runner_v1 import create_ascend_weight_offloader
|
||||
|
||||
from tests.model_executor.offloader.test_runner_integration import make_config
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("num_in_group", "expected_layers"),
|
||||
[
|
||||
(0, ()),
|
||||
(1, (3, 7, 11)),
|
||||
(2, (2, 3, 6, 7, 10, 11)),
|
||||
(4, tuple(range(12))),
|
||||
],
|
||||
)
|
||||
def test_offload_num_in_group_controls_layers_selected_within_each_group(
|
||||
num_in_group,
|
||||
expected_layers,
|
||||
):
|
||||
selection = select_offload_layers(
|
||||
num_layers=12,
|
||||
group_size=4,
|
||||
num_in_group=num_in_group,
|
||||
)
|
||||
|
||||
assert selection.layer_indices == expected_layers
|
||||
assert selection.enabled is bool(expected_layers)
|
||||
|
||||
|
||||
def test_offload_num_in_group_zero_disables_offloader_even_when_group_size_set():
|
||||
offloader = create_ascend_weight_offloader(make_config(4, 0))
|
||||
|
||||
assert isinstance(offloader, NoopOffloader)
|
||||
|
||||
|
||||
def test_offload_num_in_group_larger_than_group_size_is_rejected():
|
||||
with pytest.raises(ValueError, match="offload_num_in_group"):
|
||||
create_ascend_weight_offloader(make_config(2, 3))
|
||||
|
||||
|
||||
def test_valid_offload_num_in_group_is_preserved_on_prefetch_offloader():
|
||||
offloader = create_ascend_weight_offloader(make_config(4, 2))
|
||||
|
||||
assert isinstance(offloader, AscendPrefetchOffloader)
|
||||
assert offloader.group_size == 4
|
||||
assert offloader.num_in_group == 2
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import importlib.util
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def find_repo_root():
|
||||
for parent in Path(__file__).resolve().parents:
|
||||
if (parent / "scripts" / "analyze_parameter_optimization.py").is_file():
|
||||
return parent
|
||||
raise RuntimeError("unable to locate parameter optimization analyzer")
|
||||
|
||||
|
||||
ROOT = find_repo_root()
|
||||
|
||||
|
||||
def load_analyzer():
|
||||
path = ROOT / "scripts" / "analyze_parameter_optimization.py"
|
||||
spec = importlib.util.spec_from_file_location(path.stem, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec.loader is not None
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class ParameterOptimizationAnalysisTests(unittest.TestCase):
|
||||
def test_pooled_round_throughput_uses_each_measured_latency(self):
|
||||
analyzer = load_analyzer()
|
||||
records = [
|
||||
{
|
||||
"label": "case",
|
||||
"measured_rounds": 2,
|
||||
"output_tokens": 200,
|
||||
"round_latencies_seconds": [1.0, 2.0],
|
||||
}
|
||||
]
|
||||
self.assertEqual(
|
||||
analyzer.pooled_round_throughputs(records),
|
||||
[100.0, 50.0],
|
||||
)
|
||||
|
||||
def test_pareto_marks_equal_capacity_slower_layout_as_dominated(self):
|
||||
analyzer = load_analyzer()
|
||||
rows = [
|
||||
{
|
||||
"label": "baseline",
|
||||
"weight_storage_saved_gb": 0.0,
|
||||
"throughput_median_tok_s": 685.0,
|
||||
},
|
||||
{
|
||||
"label": "spaced_two",
|
||||
"weight_storage_saved_gb": 0.1743,
|
||||
"throughput_median_tok_s": 688.0,
|
||||
},
|
||||
{
|
||||
"label": "adjacent_two",
|
||||
"weight_storage_saved_gb": 0.1743,
|
||||
"throughput_median_tok_s": 681.0,
|
||||
},
|
||||
{
|
||||
"label": "spaced_three",
|
||||
"weight_storage_saved_gb": 0.2615,
|
||||
"throughput_median_tok_s": 663.0,
|
||||
},
|
||||
]
|
||||
analyzer.mark_pareto(rows, "baseline")
|
||||
by_label = {row["label"]: row for row in rows}
|
||||
self.assertTrue(by_label["spaced_two"]["pareto_optimal"])
|
||||
self.assertFalse(by_label["adjacent_two"]["pareto_optimal"])
|
||||
self.assertEqual(
|
||||
by_label["adjacent_two"]["dominated_by"],
|
||||
["spaced_two"],
|
||||
)
|
||||
self.assertTrue(by_label["spaced_three"]["pareto_optimal"])
|
||||
|
||||
def test_log_metadata_extracts_selected_layers_capacity_and_kv_cache(self):
|
||||
analyzer = load_analyzer()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "group_14_num_1_repeat_0.log"
|
||||
path.write_text(
|
||||
"Ascend weight offloader ready: "
|
||||
"selected_layers=(13, 27), wrapped=2, saved=0.1743 GB, "
|
||||
"static_buffer=0.0872 GB\n"
|
||||
"Available KV cache memory: 27.45 GiB\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
metadata, errors = analyzer.parse_log_metadata([path])
|
||||
self.assertEqual(errors, [])
|
||||
self.assertEqual(
|
||||
metadata["group_14_num_1"],
|
||||
{
|
||||
"selected_layers": "(13, 27)",
|
||||
"offloaded_layer_count": 2,
|
||||
"weight_storage_saved_gb": 0.1743,
|
||||
"static_buffer_gb": 0.0872,
|
||||
"kv_cache_available_median_gib": 27.45,
|
||||
},
|
||||
)
|
||||
|
||||
def test_runner_contains_complete_main_and_confirmation_matrices(self):
|
||||
script = (
|
||||
ROOT / "scripts" / "run_parameter_optimization_benchmark.sh"
|
||||
).read_text(encoding="utf-8")
|
||||
for label, count in analyzer_expected_counts().items():
|
||||
self.assertGreaterEqual(script.count(label), count)
|
||||
self.assertIn("CONFIRMATION_ROUNDS=${CONFIRMATION_ROUNDS:-10}", script)
|
||||
self.assertIn("analyze_parameter_optimization.py", script)
|
||||
self.assertIn("set -euo pipefail", script)
|
||||
|
||||
|
||||
def analyzer_expected_counts():
|
||||
analyzer = load_analyzer()
|
||||
return {
|
||||
**{label: 1 for label in analyzer.CASE_ORDER},
|
||||
**{
|
||||
label: 2
|
||||
for label in analyzer.CONFIRMATION_ORDER
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
|
@ -0,0 +1,124 @@
|
|||
import torch
|
||||
|
||||
import vllm_ascend.model_executor.offloader.prefetch as prefetch_module
|
||||
from vllm_ascend.model_executor.offloader import AscendPrefetchOffloader
|
||||
|
||||
|
||||
def make_layers(n):
|
||||
return [torch.nn.Linear(2, 2, bias=False) for _ in range(n)]
|
||||
|
||||
|
||||
class StridedWeightLayer(torch.nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.weight = torch.nn.Parameter(torch.empty_strided((2, 3), (1, 2)))
|
||||
|
||||
|
||||
def make_layers_with_strided_selected_weight():
|
||||
return [torch.nn.Linear(2, 2, bias=False) for _ in range(3)] + [
|
||||
StridedWeightLayer()
|
||||
]
|
||||
|
||||
|
||||
def test_prefetch_wraps_selected_layers_and_records_slots():
|
||||
layers = make_layers(8)
|
||||
offloader = AscendPrefetchOffloader(
|
||||
group_size=4,
|
||||
num_in_group=1,
|
||||
prefetch_step=1,
|
||||
)
|
||||
|
||||
wrapped = offloader.wrap_modules(iter(layers))
|
||||
|
||||
assert wrapped == layers
|
||||
assert offloader.selected_layer_indices == (3, 7)
|
||||
assert [m.layer_idx for m in offloader.module_offloaders] == [3, 7]
|
||||
assert [m.slot_idx for m in offloader.module_offloaders] == [0, 0]
|
||||
|
||||
|
||||
def test_prefetch_captures_pinned_cpu_storage_with_original_layout(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
prefetch_module,
|
||||
"is_pin_memory_available",
|
||||
lambda: True,
|
||||
raising=False,
|
||||
)
|
||||
layers = make_layers_with_strided_selected_weight()
|
||||
selected = layers[3]
|
||||
selected.weight.data.fill_(2.0)
|
||||
expected_shape = selected.weight.shape
|
||||
expected_stride = selected.weight.stride()
|
||||
expected_dtype = selected.weight.dtype
|
||||
offloader = AscendPrefetchOffloader(
|
||||
group_size=4,
|
||||
num_in_group=1,
|
||||
prefetch_step=1,
|
||||
)
|
||||
|
||||
offloader.wrap_modules(iter(layers))
|
||||
|
||||
state = offloader.module_offloaders[0].param_states[0]
|
||||
assert state.cpu_tensor.is_pinned()
|
||||
assert state.cpu_tensor.device.type == "cpu"
|
||||
assert state.cpu_tensor.shape == expected_shape
|
||||
assert state.cpu_tensor.stride() == expected_stride
|
||||
assert state.cpu_tensor.dtype == expected_dtype
|
||||
assert torch.equal(state.cpu_tensor, torch.full_like(state.cpu_tensor, 2.0))
|
||||
|
||||
|
||||
def test_prefetch_post_init_keeps_cpu_storage_and_counts_bytes_without_npu():
|
||||
layers = make_layers(4)
|
||||
selected = layers[3]
|
||||
selected.weight.data.fill_(2.0)
|
||||
offloader = AscendPrefetchOffloader(
|
||||
group_size=4,
|
||||
num_in_group=1,
|
||||
prefetch_step=1,
|
||||
)
|
||||
offloader.wrap_modules(iter(layers))
|
||||
|
||||
offloader.post_init()
|
||||
|
||||
module_offloader = offloader.module_offloaders[0]
|
||||
state = module_offloader.param_states[0]
|
||||
assert state.cpu_tensor.device.type == "cpu"
|
||||
assert torch.equal(state.cpu_tensor, torch.full_like(state.cpu_tensor, 2.0))
|
||||
assert offloader.total_offloaded_bytes == selected.weight.numel() * selected.weight.element_size()
|
||||
|
||||
|
||||
def test_prefetch_post_init_repins_processed_cpu_weights_when_available(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
prefetch_module,
|
||||
"is_pin_memory_available",
|
||||
lambda: True,
|
||||
raising=False,
|
||||
)
|
||||
layers = make_layers(4)
|
||||
selected = layers[3]
|
||||
offloader = AscendPrefetchOffloader(
|
||||
group_size=4,
|
||||
num_in_group=1,
|
||||
prefetch_step=1,
|
||||
)
|
||||
offloader.wrap_modules(iter(layers))
|
||||
|
||||
loaded_weight = torch.full_like(selected.weight.data, 3.0)
|
||||
selected.weight.data = loaded_weight
|
||||
offloader.post_init()
|
||||
|
||||
state = offloader.module_offloaders[0].param_states[0]
|
||||
assert state.cpu_tensor.is_pinned()
|
||||
assert state.cpu_tensor.device.type == "cpu"
|
||||
assert state.cpu_tensor.shape == loaded_weight.shape
|
||||
assert state.cpu_tensor.stride() == loaded_weight.stride()
|
||||
assert state.cpu_tensor.dtype == loaded_weight.dtype
|
||||
assert torch.equal(state.cpu_tensor, loaded_weight)
|
||||
|
||||
|
||||
def test_prefetch_zero_step_is_rejected():
|
||||
try:
|
||||
AscendPrefetchOffloader(group_size=4, num_in_group=1, prefetch_step=0)
|
||||
except ValueError as exc:
|
||||
assert "prefetch_step" in str(exc)
|
||||
else:
|
||||
raise AssertionError("expected ValueError")
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import pytest
|
||||
from types import SimpleNamespace
|
||||
|
||||
import vllm_ascend.worker.model_runner_v1 as model_runner_v1
|
||||
from vllm_ascend.model_executor.offloader import AscendPrefetchOffloader
|
||||
from vllm_ascend.worker.model_runner_v1 import create_ascend_weight_offloader
|
||||
|
||||
|
||||
def make_config(group_size, num_in_group, offload_backend="auto"):
|
||||
return SimpleNamespace(
|
||||
offload_backend=offload_backend,
|
||||
prefetch=SimpleNamespace(
|
||||
offload_group_size=group_size,
|
||||
offload_num_in_group=num_in_group,
|
||||
offload_params=set(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_uva_backend_preserves_parent_created_offloader(monkeypatch):
|
||||
parent_offloader = object()
|
||||
monkeypatch.setattr(model_runner_v1, "get_offloader", lambda: parent_offloader)
|
||||
|
||||
offloader = create_ascend_weight_offloader(
|
||||
make_config(4, 1, offload_backend="uva")
|
||||
)
|
||||
|
||||
assert offloader is parent_offloader
|
||||
|
||||
|
||||
def test_auto_backend_with_zero_group_size_preserves_parent_created_offloader(
|
||||
monkeypatch,
|
||||
):
|
||||
parent_offloader = object()
|
||||
monkeypatch.setattr(model_runner_v1, "get_offloader", lambda: parent_offloader)
|
||||
|
||||
offloader = create_ascend_weight_offloader(
|
||||
make_config(0, 1, offload_backend="auto")
|
||||
)
|
||||
|
||||
assert offloader is parent_offloader
|
||||
|
||||
|
||||
def test_auto_backend_with_positive_group_size_replaces_parent_with_prefetch(
|
||||
monkeypatch,
|
||||
):
|
||||
parent_offloader = object()
|
||||
monkeypatch.setattr(model_runner_v1, "get_offloader", lambda: parent_offloader)
|
||||
|
||||
offloader = create_ascend_weight_offloader(
|
||||
make_config(4, 1, offload_backend="auto")
|
||||
)
|
||||
|
||||
assert isinstance(offloader, AscendPrefetchOffloader)
|
||||
assert offloader is not parent_offloader
|
||||
assert offloader.group_size == 4
|
||||
assert offloader.num_in_group == 1
|
||||
assert offloader.prefetch_step == 1
|
||||
|
||||
|
||||
def test_prefetch_backend_replaces_parent_created_offloader(monkeypatch):
|
||||
parent_offloader = object()
|
||||
monkeypatch.setattr(model_runner_v1, "get_offloader", lambda: parent_offloader)
|
||||
|
||||
offloader = create_ascend_weight_offloader(
|
||||
make_config(4, 1, offload_backend="prefetch")
|
||||
)
|
||||
|
||||
assert isinstance(offloader, AscendPrefetchOffloader)
|
||||
assert offloader is not parent_offloader
|
||||
|
||||
|
||||
def test_negative_group_size_raises():
|
||||
with pytest.raises(ValueError, match="offload_group_size must be >= 0"):
|
||||
create_ascend_weight_offloader(make_config(-1, 1))
|
||||
|
||||
|
||||
def test_num_in_group_larger_than_group_size_raises():
|
||||
with pytest.raises(ValueError, match="offload_num_in_group"):
|
||||
create_ascend_weight_offloader(make_config(2, 3))
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import pytest
|
||||
|
||||
from vllm_ascend.model_executor.offloader.selection import (
|
||||
OffloadSelection,
|
||||
select_offload_layers,
|
||||
)
|
||||
|
||||
|
||||
def test_group_size_zero_disables_offload():
|
||||
selection = select_offload_layers(num_layers=8, group_size=0, num_in_group=1)
|
||||
assert selection == OffloadSelection(enabled=False, layer_indices=())
|
||||
|
||||
|
||||
def test_num_in_group_zero_selects_no_layers():
|
||||
selection = select_offload_layers(num_layers=8, group_size=4, num_in_group=0)
|
||||
assert selection == OffloadSelection(enabled=False, layer_indices=())
|
||||
|
||||
|
||||
def test_num_in_group_greater_than_group_size_raises():
|
||||
with pytest.raises(ValueError, match="offload_num_in_group .* must be <= offload_group_size"):
|
||||
select_offload_layers(num_layers=8, group_size=2, num_in_group=3)
|
||||
|
||||
|
||||
def test_group_size_four_num_one_selects_last_layer_of_each_group():
|
||||
selection = select_offload_layers(num_layers=16, group_size=4, num_in_group=1)
|
||||
assert selection == OffloadSelection(enabled=True, layer_indices=(3, 7, 11, 15))
|
||||
|
||||
|
||||
def test_group_size_eight_num_two_selects_last_two_layers_of_each_group():
|
||||
selection = select_offload_layers(num_layers=24, group_size=8, num_in_group=2)
|
||||
assert selection == OffloadSelection(enabled=True, layer_indices=(6, 7, 14, 15, 22, 23))
|
||||
|
||||
|
||||
def test_num_in_group_equal_group_size_selects_all_layers():
|
||||
selection = select_offload_layers(num_layers=5, group_size=2, num_in_group=2)
|
||||
assert selection == OffloadSelection(enabled=True, layer_indices=(0, 1, 2, 3, 4))
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm_ascend.model_executor.offloader.sync import AscendSyncOffloader
|
||||
|
||||
|
||||
class TinyLayer(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.linear = nn.Linear(2, 2, bias=False)
|
||||
|
||||
def forward(self, x):
|
||||
return self.linear(x)
|
||||
|
||||
|
||||
def test_disabled_config_returns_modules_unchanged():
|
||||
modules = [TinyLayer(), TinyLayer()]
|
||||
offloader = AscendSyncOffloader(group_size=0, num_in_group=1)
|
||||
|
||||
wrapped = offloader.wrap_modules(iter(modules))
|
||||
|
||||
assert wrapped == modules
|
||||
assert offloader.selected_layer_indices == ()
|
||||
|
||||
|
||||
def test_selects_matching_layers_without_touching_unselected_layers():
|
||||
modules = [TinyLayer() for _ in range(4)]
|
||||
offloader = AscendSyncOffloader(group_size=4, num_in_group=1)
|
||||
|
||||
wrapped = offloader.wrap_modules(iter(modules))
|
||||
|
||||
assert wrapped == modules
|
||||
assert offloader.selected_layer_indices == (3,)
|
||||
assert len(offloader.module_offloaders) == 1
|
||||
assert offloader.module_offloaders[0].layer_idx == 3
|
||||
|
||||
|
||||
def test_invalid_num_in_group_raises():
|
||||
offloader = AscendSyncOffloader(group_size=2, num_in_group=3)
|
||||
|
||||
try:
|
||||
offloader.wrap_modules(iter([TinyLayer(), TinyLayer()]))
|
||||
except ValueError as exc:
|
||||
assert "offload_num_in_group" in str(exc)
|
||||
else:
|
||||
raise AssertionError("expected ValueError")
|
||||
|
||||
|
||||
def test_post_init_syncs_loaded_weights_to_cpu_storage():
|
||||
layer = torch.nn.Linear(2, 2, bias=False)
|
||||
modules = [torch.nn.Linear(2, 2, bias=False) for _ in range(3)] + [layer]
|
||||
offloader = AscendSyncOffloader(group_size=4, num_in_group=1)
|
||||
|
||||
offloader.wrap_modules(iter(modules))
|
||||
loaded_weight = torch.full_like(layer.weight.data, 3.0)
|
||||
layer.weight.data = loaded_weight
|
||||
|
||||
offloader.post_init()
|
||||
|
||||
assert len(offloader.module_offloaders) == 1
|
||||
state = offloader.module_offloaders[0].param_states[0]
|
||||
assert state.param.device.type == "cpu"
|
||||
assert torch.equal(state.cpu_tensor, loaded_weight.cpu())
|
||||
assert offloader.total_offloaded_bytes == loaded_weight.numel() * loaded_weight.element_size()
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
from vllm_ascend.model_executor.offloader.selection import (
|
||||
OffloadSelection,
|
||||
select_offload_layers,
|
||||
)
|
||||
from vllm_ascend.model_executor.offloader.prefetch import AscendPrefetchOffloader
|
||||
from vllm_ascend.model_executor.offloader.sync import AscendSyncOffloader
|
||||
|
||||
__all__ = [
|
||||
"AscendPrefetchOffloader",
|
||||
"AscendSyncOffloader",
|
||||
"OffloadSelection",
|
||||
"select_offload_layers",
|
||||
]
|
||||
|
|
@ -0,0 +1,274 @@
|
|||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.offloader.base import BaseOffloader
|
||||
from vllm.utils.platform_utils import is_pin_memory_available
|
||||
from vllm_ascend.model_executor.offloader.selection import select_offload_layers
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ParamState:
|
||||
name: str
|
||||
param: nn.Parameter
|
||||
cpu_tensor: torch.Tensor
|
||||
original_device: torch.device
|
||||
num_bytes: int
|
||||
static_tensor: torch.Tensor | None = None
|
||||
|
||||
|
||||
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: dict[tuple[str, tuple[int, ...], tuple[int, ...], torch.dtype], list[torch.Tensor]] = {}
|
||||
|
||||
@staticmethod
|
||||
def _num_bytes(tensor: torch.Tensor) -> int:
|
||||
return tensor.numel() * tensor.element_size()
|
||||
|
||||
def get_buffer(self, state: _ParamState, slot_idx: int) -> torch.Tensor:
|
||||
key = (
|
||||
state.name,
|
||||
tuple(state.cpu_tensor.shape),
|
||||
tuple(state.cpu_tensor.stride()),
|
||||
state.cpu_tensor.dtype,
|
||||
)
|
||||
if key not in self._buffers:
|
||||
buffers = []
|
||||
for _ in range(self.slot_capacity):
|
||||
buffer = torch.empty_strided(
|
||||
size=state.cpu_tensor.shape,
|
||||
stride=state.cpu_tensor.stride(),
|
||||
dtype=state.cpu_tensor.dtype,
|
||||
device=self.device,
|
||||
)
|
||||
buffers.append(buffer)
|
||||
self.total_bytes += self._num_bytes(buffer)
|
||||
self._buffers[key] = buffers
|
||||
return self._buffers[key][slot_idx % self.slot_capacity]
|
||||
|
||||
|
||||
class _PrefetchModuleOffloader:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
module: nn.Module,
|
||||
layer_idx: int,
|
||||
slot_idx: int,
|
||||
offload_params: set[str],
|
||||
):
|
||||
self.module = module
|
||||
self.layer_idx = layer_idx
|
||||
self.slot_idx = slot_idx
|
||||
self.offload_params = offload_params
|
||||
self.param_states: list[_ParamState] = []
|
||||
self.offloaded_bytes = 0
|
||||
self.copy_done_event = None
|
||||
self._capture_params()
|
||||
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _tensor_nbytes(tensor: torch.Tensor) -> int:
|
||||
return tensor.numel() * tensor.element_size()
|
||||
|
||||
@staticmethod
|
||||
def _copy_to_cpu_storage(tensor: torch.Tensor) -> torch.Tensor:
|
||||
cpu_tensor = torch.empty_strided(
|
||||
size=tensor.shape,
|
||||
stride=tensor.stride(),
|
||||
dtype=tensor.dtype,
|
||||
device="cpu",
|
||||
pin_memory=is_pin_memory_available(),
|
||||
)
|
||||
cpu_tensor.copy_(tensor.detach())
|
||||
return cpu_tensor
|
||||
|
||||
def _capture_params(self) -> None:
|
||||
for name, param in self.module.named_parameters(recurse=True):
|
||||
if not self._matches_param(name):
|
||||
continue
|
||||
cpu_tensor = self._copy_to_cpu_storage(param)
|
||||
self.param_states.append(
|
||||
_ParamState(
|
||||
name=name,
|
||||
param=param,
|
||||
cpu_tensor=cpu_tensor,
|
||||
original_device=param.device,
|
||||
num_bytes=self._tensor_nbytes(cpu_tensor),
|
||||
)
|
||||
)
|
||||
param.data = cpu_tensor
|
||||
|
||||
def post_init(self, pool: _StaticBufferPool | None) -> None:
|
||||
self.offloaded_bytes = 0
|
||||
for state in self.param_states:
|
||||
state.cpu_tensor = self._copy_to_cpu_storage(state.param)
|
||||
state.num_bytes = self._tensor_nbytes(state.cpu_tensor)
|
||||
self.offloaded_bytes += state.num_bytes
|
||||
if pool is None:
|
||||
state.param.data = state.cpu_tensor
|
||||
continue
|
||||
state.static_tensor = pool.get_buffer(state, self.slot_idx)
|
||||
state.param.data = state.static_tensor
|
||||
|
||||
def start_prefetch(self, copy_stream) -> None:
|
||||
if copy_stream is None:
|
||||
return
|
||||
if not self.param_states or self.param_states[0].static_tensor is None:
|
||||
return
|
||||
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):
|
||||
for state in self.param_states:
|
||||
assert state.static_tensor is not None
|
||||
state.static_tensor.copy_(state.cpu_tensor, non_blocking=True)
|
||||
self.copy_done_event = torch.npu.Event()
|
||||
self.copy_done_event.record(copy_stream)
|
||||
|
||||
def wait_prefetch(self) -> None:
|
||||
if self.copy_done_event is not None:
|
||||
torch.npu.current_stream().wait_event(self.copy_done_event)
|
||||
|
||||
|
||||
class AscendPrefetchOffloader(BaseOffloader):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
group_size: int,
|
||||
num_in_group: int,
|
||||
prefetch_step: int,
|
||||
offload_params: set[str] | None = None,
|
||||
):
|
||||
if prefetch_step <= 0:
|
||||
raise ValueError("prefetch_step must be > 0")
|
||||
self.group_size = group_size
|
||||
self.num_in_group = num_in_group
|
||||
self.prefetch_step = prefetch_step
|
||||
self.offload_params = offload_params or set()
|
||||
self.selected_layer_indices: tuple[int, ...] = ()
|
||||
self.module_offloaders: list[_PrefetchModuleOffloader] = []
|
||||
self.total_offloaded_bytes = 0
|
||||
self.static_buffer_bytes = 0
|
||||
self.buffer_pool: _StaticBufferPool | None = None
|
||||
self.copy_stream = None
|
||||
|
||||
@staticmethod
|
||||
def _npu_available() -> bool:
|
||||
return hasattr(torch, "npu") and torch.npu.is_available()
|
||||
|
||||
def _get_device(self) -> torch.device | None:
|
||||
for module_offloader in self.module_offloaders:
|
||||
for state in module_offloader.param_states:
|
||||
if state.original_device.type != "cpu":
|
||||
return state.original_device
|
||||
if self._npu_available():
|
||||
return torch.device("npu")
|
||||
return None
|
||||
|
||||
def wrap_modules(
|
||||
self,
|
||||
modules_generator: Generator[nn.Module, None, None],
|
||||
) -> list[nn.Module]:
|
||||
modules = list(modules_generator)
|
||||
selection = select_offload_layers(
|
||||
num_layers=len(modules),
|
||||
group_size=self.group_size,
|
||||
num_in_group=self.num_in_group,
|
||||
)
|
||||
self.selected_layer_indices = selection.layer_indices
|
||||
if not selection.enabled:
|
||||
return modules
|
||||
|
||||
offload_position = 0
|
||||
for layer_idx in selection.layer_indices:
|
||||
module = modules[layer_idx]
|
||||
if not any(True for _ in module.parameters(recurse=True)):
|
||||
continue
|
||||
module_offloader = _PrefetchModuleOffloader(
|
||||
module=module,
|
||||
layer_idx=layer_idx,
|
||||
slot_idx=offload_position % self.prefetch_step,
|
||||
offload_params=self.offload_params,
|
||||
)
|
||||
if not module_offloader.param_states:
|
||||
continue
|
||||
self.module_offloaders.append(module_offloader)
|
||||
self._install_forward_hook(offload_position, module_offloader)
|
||||
offload_position += 1
|
||||
|
||||
logger.info(
|
||||
"[AscendPrefetchOffloader] selected_layers=%s, wrapped=%d, "
|
||||
"group_size=%d, num_in_group=%d, prefetch_step=%d",
|
||||
self.selected_layer_indices,
|
||||
len(self.module_offloaders),
|
||||
self.group_size,
|
||||
self.num_in_group,
|
||||
self.prefetch_step,
|
||||
)
|
||||
return modules
|
||||
|
||||
def _install_forward_hook(
|
||||
self,
|
||||
offload_position: int,
|
||||
module_offloader: _PrefetchModuleOffloader,
|
||||
) -> None:
|
||||
original_forward = module_offloader.module.forward
|
||||
|
||||
def forward(*args, **kwargs):
|
||||
module_offloader.wait_prefetch()
|
||||
output = original_forward(*args, **kwargs)
|
||||
if self.module_offloaders:
|
||||
next_position = (offload_position + self.prefetch_step) % len(self.module_offloaders)
|
||||
self.module_offloaders[next_position].start_prefetch(self.copy_stream)
|
||||
return output
|
||||
|
||||
module_offloader.module.forward = forward
|
||||
|
||||
def post_init(self) -> None:
|
||||
self.total_offloaded_bytes = 0
|
||||
device = self._get_device()
|
||||
if device is not None and self._npu_available():
|
||||
self.copy_stream = torch.npu.Stream(device=device)
|
||||
self.buffer_pool = _StaticBufferPool(
|
||||
slot_capacity=self.prefetch_step,
|
||||
device=device,
|
||||
)
|
||||
else:
|
||||
self.copy_stream = None
|
||||
self.buffer_pool = None
|
||||
|
||||
for module_offloader in self.module_offloaders:
|
||||
module_offloader.post_init(self.buffer_pool)
|
||||
self.total_offloaded_bytes += module_offloader.offloaded_bytes
|
||||
|
||||
self.static_buffer_bytes = 0 if self.buffer_pool is None else self.buffer_pool.total_bytes
|
||||
logger.info(
|
||||
"[AscendPrefetchOffloader] initialized wrapped=%d, saved=%.4f GB, "
|
||||
"static_buffer=%.4f GB, selected_layers=%s",
|
||||
len(self.module_offloaders),
|
||||
self.total_offloaded_bytes / float(2**30),
|
||||
self.static_buffer_bytes / float(2**30),
|
||||
self.selected_layer_indices,
|
||||
)
|
||||
|
||||
for module_offloader in self.module_offloaders[: self.prefetch_step]:
|
||||
module_offloader.start_prefetch(self.copy_stream)
|
||||
|
||||
def sync_prev_onload(self) -> None:
|
||||
if self.copy_stream is not None:
|
||||
torch.npu.current_stream().wait_stream(self.copy_stream)
|
||||
|
||||
def join_after_forward(self) -> None:
|
||||
self.sync_prev_onload()
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OffloadSelection:
|
||||
enabled: bool
|
||||
layer_indices: tuple[int, ...]
|
||||
|
||||
|
||||
def select_offload_layers(
|
||||
*,
|
||||
num_layers: int,
|
||||
group_size: int,
|
||||
num_in_group: int,
|
||||
) -> OffloadSelection:
|
||||
if group_size < 0:
|
||||
raise ValueError("offload_group_size must be >= 0")
|
||||
if num_in_group < 0:
|
||||
raise ValueError("offload_num_in_group must be >= 0")
|
||||
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(
|
||||
f"offload_num_in_group ({num_in_group}) must be <= "
|
||||
f"offload_group_size ({group_size})"
|
||||
)
|
||||
|
||||
cutoff = group_size - num_in_group
|
||||
indices = tuple(
|
||||
layer_idx
|
||||
for layer_idx in range(num_layers)
|
||||
if layer_idx % group_size >= cutoff
|
||||
)
|
||||
return OffloadSelection(enabled=bool(indices), layer_indices=indices)
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
from collections.abc import Generator
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.offloader.base import BaseOffloader
|
||||
from vllm_ascend.model_executor.offloader.selection import select_offload_layers
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ParamState:
|
||||
name: str
|
||||
param: nn.Parameter
|
||||
cpu_tensor: torch.Tensor
|
||||
original_device: torch.device
|
||||
num_bytes: int
|
||||
|
||||
|
||||
class _SyncModuleOffloader:
|
||||
def __init__(self, *, module: nn.Module, layer_idx: int, offload_params: set[str]):
|
||||
self.module = module
|
||||
self.layer_idx = layer_idx
|
||||
self.param_states: list[_ParamState] = []
|
||||
self.offload_params = offload_params
|
||||
self.offloaded_bytes = 0
|
||||
self._original_forward = module.forward
|
||||
self._capture_params()
|
||||
self._install_forward_hook()
|
||||
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _tensor_nbytes(tensor: torch.Tensor) -> int:
|
||||
return tensor.numel() * tensor.element_size()
|
||||
|
||||
def _capture_params(self) -> None:
|
||||
for name, param in self.module.named_parameters(recurse=True):
|
||||
if not self._matches_param(name):
|
||||
continue
|
||||
cpu_tensor = param.detach().cpu().contiguous()
|
||||
self.param_states.append(
|
||||
_ParamState(
|
||||
name=name,
|
||||
param=param,
|
||||
cpu_tensor=cpu_tensor,
|
||||
original_device=param.device,
|
||||
num_bytes=self._tensor_nbytes(cpu_tensor),
|
||||
)
|
||||
)
|
||||
param.data = cpu_tensor
|
||||
|
||||
def post_init(self) -> None:
|
||||
self.offloaded_bytes = 0
|
||||
for state in self.param_states:
|
||||
state.cpu_tensor = state.param.detach().cpu().contiguous()
|
||||
state.num_bytes = self._tensor_nbytes(state.cpu_tensor)
|
||||
state.param.data = state.cpu_tensor
|
||||
self.offloaded_bytes += state.num_bytes
|
||||
|
||||
@staticmethod
|
||||
def _device_from_args(args, kwargs) -> torch.device | None:
|
||||
tensors = list(args) + list(kwargs.values())
|
||||
for value in tensors:
|
||||
if isinstance(value, torch.Tensor) and value.device.type != "cpu":
|
||||
return value.device
|
||||
if isinstance(value, (tuple, list)):
|
||||
for item in value:
|
||||
if isinstance(item, torch.Tensor) and item.device.type != "cpu":
|
||||
return item.device
|
||||
return None
|
||||
|
||||
def _onload(self, args, kwargs) -> None:
|
||||
runtime_device = self._device_from_args(args, kwargs)
|
||||
for state in self.param_states:
|
||||
target_device = state.original_device
|
||||
if target_device.type == "cpu" and runtime_device is not None:
|
||||
target_device = runtime_device
|
||||
if target_device.type == "cpu":
|
||||
continue
|
||||
if state.param.device != target_device:
|
||||
state.param.data = state.cpu_tensor.to(
|
||||
device=target_device,
|
||||
non_blocking=False,
|
||||
)
|
||||
|
||||
def _offload(self) -> None:
|
||||
for state in self.param_states:
|
||||
if state.param.device.type != "cpu":
|
||||
state.param.data = state.cpu_tensor
|
||||
|
||||
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
|
||||
|
||||
|
||||
class AscendSyncOffloader(BaseOffloader):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
group_size: int,
|
||||
num_in_group: int,
|
||||
offload_params: set[str] | None = None,
|
||||
):
|
||||
self.group_size = group_size
|
||||
self.num_in_group = num_in_group
|
||||
self.offload_params = offload_params or set()
|
||||
self.selected_layer_indices: tuple[int, ...] = ()
|
||||
self.module_offloaders: list[_SyncModuleOffloader] = []
|
||||
self.total_offloaded_bytes = 0
|
||||
|
||||
def wrap_modules(
|
||||
self,
|
||||
modules_generator: Generator[nn.Module, None, None],
|
||||
) -> list[nn.Module]:
|
||||
modules = list(modules_generator)
|
||||
selection = select_offload_layers(
|
||||
num_layers=len(modules),
|
||||
group_size=self.group_size,
|
||||
num_in_group=self.num_in_group,
|
||||
)
|
||||
self.selected_layer_indices = selection.layer_indices
|
||||
if not selection.enabled:
|
||||
return modules
|
||||
|
||||
for layer_idx in selection.layer_indices:
|
||||
module = modules[layer_idx]
|
||||
if not any(True for _ in module.parameters(recurse=True)):
|
||||
continue
|
||||
module_offloader = _SyncModuleOffloader(
|
||||
module=module,
|
||||
layer_idx=layer_idx,
|
||||
offload_params=self.offload_params,
|
||||
)
|
||||
if not module_offloader.param_states:
|
||||
continue
|
||||
self.module_offloaders.append(module_offloader)
|
||||
|
||||
logger.info(
|
||||
"[AscendSyncOffloader] selected_layers=%s, wrapped=%d, "
|
||||
"group_size=%d, num_in_group=%d",
|
||||
self.selected_layer_indices,
|
||||
len(self.module_offloaders),
|
||||
self.group_size,
|
||||
self.num_in_group,
|
||||
)
|
||||
return modules
|
||||
|
||||
def post_init(self) -> None:
|
||||
self.total_offloaded_bytes = 0
|
||||
for module_offloader in self.module_offloaders:
|
||||
module_offloader.post_init()
|
||||
self.total_offloaded_bytes += module_offloader.offloaded_bytes
|
||||
logger.info(
|
||||
"[AscendSyncOffloader] initialized wrapped=%d, saved=%.4f GB, "
|
||||
"selected_layers=%s",
|
||||
len(self.module_offloaders),
|
||||
self.total_offloaded_bytes / float(2**30),
|
||||
self.selected_layer_indices,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue