2
DCU技术达人
我把 Qwen2.5-Coder-0.5B 跑成了代码审查 API:海光 BW、vLLM 与 8 并发实测
2026-09-28 18:05:55
12
# 我把 Qwen2.5-Coder-0.5B 跑成了代码审查 API:海光 BW、vLLM 与 8 并发实测 https://n-2082711171478953986.zzai.scnet.cn:58043/jupyter-forward/2082711171478953986/lab/tree/root ![1790589697866.png](/api-forum/file/RedirectImage?path=20260928/1790589719687_1790589697866.png '1790589697866.png') 第一篇里,我用 Transformers 验证了 Qwen 权重能在海光 BW 上离线推理。第二次登录服务器时,我不想再写一个“输入问题、终端打印答案”的脚本了。真正接应用时,调用方需要的是 HTTP 接口,而不是某个 Python 进程里的 `generate()`。 我因此把 Qwen2.5-Coder-0.5B-Instruct 交给 vLLM,目标也定得很具体: ```text 模型能加载 -> 服务能监听端口 -> /v1/models 能返回 -> /v1/chat/completions 能完成请求 -> 代码答案能被人工和测试复核 -> 1/2/4/8 并发有可重复数据 ``` 过程并没有一遍过。第一次后台启动后端口始终没有起来,问题最后落在 Bash 的 `set -u`;服务跑通以后,0.5B Coder 又给了几段“解释正确、修复代码却原样不动”的答案。这两件事比一个漂亮的成功截图更值得写下来。 补测后的结果是:并发从 1 增到 8,聚合生成速度从 34.71 提升到 190.33 token/s;并发 4 时 P95 延迟为 1.5690 秒,并发 8 时升到 2.7509 秒。对这组短请求而言,4 并发附近是一个更舒服的平衡点——但这只是当前模型、当前提示词下的观察,不是设备的容量上限。 > 文中的日志、延迟和吞吐都来自这台实例。vLLM 采用 `--enforce-eager` 建立兼容性基线,未开启编译和 CUDAGraph,因此数据不能代表优化后的最高性能。 ## 我为什么选 0.5B Coder,而不是一开始就追求大模型 这次使用的模型是: ModelScope 页面标注约 494.03M 参数、快照约 999.60MB,许可证为 Apache-2.0。我在可联网电脑完成下载,再把 11 个文件传到海光节点。远端目录约 954MiB,核心权重哈希为: ```text f9523886352217ded3aeeef552b381af79d568c6d49a4b9e423288cea56b0a44 ``` 选择这个尺寸不是因为它足以做生产级代码审查,而是因为我想先固定服务框架。只要 OpenAI 兼容协议、日志、客户端和并发脚本不变,后面替换更大的 Coder 模型时,业务侧不需要跟着重写。 当前环境: | 项目 | 实测值 | | --- | --- | | 设备 | Hygon BW | | 设备显存 | 63.98GiB | | Python | 3.10.12 | | PyTorch | 2.10.0 | | HIP | 6.3.26093 | | vLLM | 0.18.1(海光环境构建) | | 权重精度 | BF16 | | 最大上下文 | 4096 token | | 服务监听 | `127.0.0.1:8008` | 我只让服务监听回环地址。这个实验接口没有鉴权,直接暴露到公网是不负责任的。如果以后真要让其他机器访问,我会在它前面增加反向代理、鉴权、限流和 TLS。 ## 第一次启动为什么没成功 我最初写的脚本很“标准”: ```bash #!/usr/bin/env bash set -euo pipefail source /opt/dtk/env.sh vllm serve ... ``` 后台执行以后,日志没有进入权重加载,`curl` 也一直连接失败。尾部只有一句: ```text /opt/dtk/env.sh: line 26: CMAKE_PREFIX_PATH: unbound variable ``` 问题和模型、显存都无关。`set -u` 会在读取未定义变量时立即终止,而平台环境脚本初始化过程中需要先处理 `CMAKE_PREFIX_PATH`。我把顺序改成先加载环境,再开启 `nounset`: ```bash #!/usr/bin/env bash set -eo pipefail MODEL_DIR="${1:-/root/qwen-modelscope-labs/models/Qwen2.5-Coder-0.5B-Instruct}" PORT="${PORT:-8008}" LOG_DIR="${LOG_DIR:-/root/qwen-modelscope-labs/logs}" mkdir -p "${LOG_DIR}" source /opt/dtk/env.sh set -u exec vllm serve "${MODEL_DIR}" \ --served-model-name qwen25-coder-0.5b \ --dtype bfloat16 \ --max-model-len 4096 \ --gpu-memory-utilization 0.35 \ --host 127.0.0.1 \ --port "${PORT}" \ --enforce-eager ``` 这个修改看起来只挪了两行,实际很关键。很多部署问题并不是“国产加速卡不兼容”,而是启动脚本沿用了另一套环境假设。先把日志读完整,比反复换模型参数有效得多。 ## 我如何确认服务不是“假启动” 第二次启动后,进程很快出现,但端口仍要等一段时间。我没有看到 PID 就宣布成功,而是等四个信号都满足: 1. 日志出现 `Platform plugin hcu is activated`; 2. 架构解析为 `Qwen2ForCausalLM`; 3. 日志出现 `Application startup complete`; 4. `/v1/models` 实际返回 HTTP 200。 这一轮日志还记录了: ```text Loading weights took 0.20 seconds Available KV cache memory: 20.46 GiB GPU KV cache size: 1,788,224 tokens ``` ![1790590004087.png](/api-forum/file/RedirectImage?path=20260928/1790590025045_1790590004087.png '1790590004087.png') 接口检查: ```bash curl -s \ http://127.0.0.1:8008/v1/models \ | python -m json.tool ``` 返回的模型 ID 是 `qwen25-coder-0.5b`,最大上下文为 4096。到这里,我才把服务状态记为可用。 日志里还给出了“Maximum concurrency for 4,096 tokens per request: 436.58x”。这个数字来自 KV Cache 理论容量,不能直接当成业务并发。真实请求还会受到提示词长度、输出长度、排队和延迟目标影响,所以我后面自己跑了并发矩阵。 ## 节点不能装 SDK,我就用标准库写客户端 远端外网不可达,为了调用一个本地 HTTP 接口再安装 OpenAI SDK 没必要。我用 `urllib.request` 写了一个最小客户端: ```python import json import time import urllib.request def chat(prompt: str, max_tokens: int = 256): payload = { "model": "qwen25-coder-0.5b", "messages": [ { "role": "system", "content": ( "你是严谨的 Python 代码审查员。" "指出可复现的问题并给出可运行修改。" ), }, {"role": "user", "content": prompt}, ], "temperature": 0, "max_tokens": max_tokens, } request = urllib.request.Request( "http://127.0.0.1:8008/v1/chat/completions", data=json.dumps( payload, ensure_ascii=False, ).encode("utf-8"), headers={"Content-Type": "application/json"}, method="POST", ) started = time.perf_counter() with urllib.request.urlopen( request, timeout=180, ) as response: body = json.loads( response.read().decode("utf-8") ) return { "latency": time.perf_counter() - started, "usage": body.get("usage"), "answer": body["choices"][0]["message"]["content"], } ``` 标准库客户端有两个好处:部署包更小,而且请求体、超时和返回 token 数都看得很清楚。等接口行为确认后,业务项目再换官方 SDK 也不迟。 ## 三个代码任务,模型到底答得怎么样 我没有用“写一个 Hello World”来证明 Coder 能工作,而是故意给了三个容易产生伪正确答案的任务。 ### 去重排序:找到了复杂度问题,却没有真正改代码 输入函数: ```python def unique_sorted(values): result = [] for value in values: if value not in result: result.append(value) return sorted(result) ``` 模型指出 `value not in result` 会反复做线性扫描,最坏复杂度达到 $O(n^2)$。判断是对的,但它给出的“修改版”仍然是原代码,随后还重复了几段复杂度说明。 如果元素可哈希,我会改成: ```python def unique_sorted(values): return sorted(set(values)) ``` 这让我确认了一件事:自然语言里的“已修复”不能作数,必须比较补丁是否真的改变了程序。 ### pytest:有测试的样子,但函数签名对不上 模型生成了 `@pytest.mark.parametrize`,却没有让测试函数接收 `values` 和 `expected`,也没有真正验证原列表未被修改。代码块看起来完整,直接执行会失败。 我最后采用的是: ```python import pytest @pytest.mark.parametrize( ("values", "expected"), [ ([3, 1, 3, 2], [1, 2, 3]), ([], []), ([-1, -3, -1, 2], [-3, -1, 2]), ], ) def test_unique_sorted(values, expected): original = list(values) assert unique_sorted(values) == expected assert values == original ``` ### 原子写入:解释提到了原子性,代码仍是普通覆盖 第三个任务要求修复并发写 JSON。模型分析了覆盖风险,最终给出的函数却仍然是 `open(path, "w")` 加 `json.dump()`。它说自己使用了原子写入,代码并没有。 同一文件系统内,我会先写临时文件,再调用 `os.replace`: ```python import json import os import tempfile from pathlib import Path def save_json(path, data): target = Path(path) target.parent.mkdir( parents=True, exist_ok=True, ) fd, temp_name = tempfile.mkstemp( dir=target.parent, prefix=f".{target.name}.", suffix=".tmp", ) try: with os.fdopen( fd, "w", encoding="utf-8", ) as handle: json.dump( data, handle, ensure_ascii=False, ) handle.flush() os.fsync(handle.fileno()) os.replace(temp_name, target) except Exception: try: os.unlink(temp_name) except FileNotFoundError: pass raise ``` 这仍然只能保证读者不会看到半个文件。如果多个写入者存在业务冲突,还需要锁、版本号或数据库事务。 三个任务跑下来,我对 0.5B Coder 的定位很明确:它适合验证接口、生成草稿、辅助教学,不适合无人复核地修改仓库。模型答案必须继续经过格式化、静态检查、单元测试和沙箱执行。 ## 第一组 4 并发,只能算冒烟 最初我只做了四个并发短请求。墙钟时间 5.0809 秒,共生成 384 token,聚合速度 75.58 token/s。 ![1790589843682.png](/api-forum/file/RedirectImage?path=20260928/1790589864614_1790589843682.png '1790589843682.png') 这组数据能证明 vLLM 在调度并发请求,但样本太少,而且和三条较长代码任务混在同一轮里。我后来重新写了一个并发矩阵脚本,每档都先预热,再重复三轮。 ## 补测 1/2/4/8 并发:8 并发吞吐更高,尾延迟开始抬头 矩阵测试使用同一类短提示词,单条最多输出 96 token。每个并发档位重复三轮,所有请求都返回 HTTP 200。 | 并发 | 总请求数 | 聚合吞吐 | 请求吞吐 | P50 延迟 | P95 延迟 | | ---: | ---: | ---: | ---: | ---: | ---: | | 1 | 3 | 34.71 token/s | 0.643 req/s | 1.5531s | 1.5594s | | 2 | 6 | 67.62 token/s | 1.284 req/s | 1.5298s | 1.6074s | | 4 | 12 | 134.61 token/s | 2.556 req/s | 1.5374s | 1.5690s | | 8 | 24 | 190.33 token/s | 2.910 req/s | 1.5991s | 2.7509s | ![1790589858136.png](/api-forum/file/RedirectImage?path=20260928/1790589879009_1790589858136.png '1790589858136.png') 从 1 到 4 并发,聚合吞吐接近线性增长,P95 仍在 1.6 秒左右。到 8 并发时,吞吐继续增加,但 P95 升到 2.7509 秒,请求吞吐的增幅也明显变小。 我不会仅凭四个档位就宣布“最佳并发是 4”,但对当前这类短请求,4 是更平衡的起点。如果目标是离线批量处理,8 并发的吞吐更有吸引力;如果目标是交互式代码助手,我会先守住尾延迟,再逐步加压。 这轮还没有测首 token 延迟,也没有持续半小时以上。正式容量测试还要补输入/输出长度分桶、错误率、P99、服务端队列和持续显存曲线。 ## `hy-smi` 的一张 0% 利用率截图,不能推翻请求日志 vLLM 按 `--gpu-memory-utilization 0.35` 预留显存后,`hy-smi` 显示 VRAM 35%。我截取的某个瞬间,HCU 利用率恰好是 0%。 ![1790589873723.png](/api-forum/file/RedirectImage?path=20260928/1790589894682_1790589873723.png '1790589873723.png') 这种瞬时值很容易被误读。请求日志已经记录了连续的 HTTP 200、生成 token 和吞吐,客户端也拿到了完整响应。单张快照只能说明采样那一刻的状态,不能代表整个请求窗口的平均利用率。 如果要做宣传或正式评测,我会按固定间隔连续采样,并把利用率曲线和请求时间线对齐。挑一张最高值或最低值都不够严谨。 ## 这次部署里海光价值 - 标准 Safetensors 权重能够被识别; - Qwen2 架构、BF16 和 KV Cache 正常工作; - vLLM 自动激活 `hcu` 平台插件; - OpenAI 兼容接口可以沿用现有客户端; - 1、2、4、8 并发都有稳定的 HTTP 响应; - 64GB 级显存给更大模型和更长上下文留下了空间。 对个人开发者来说,这种迁移平滑度比口号有用。我可以保留原来的模型目录、API 协议和测试代码,把精力放在启动环境、参数和验收上,而不是从头重做应用。 实验结束后,我确认了 vLLM 主进程号,再发送终止信号,`hy-smi` 中的 VRAM 占用恢复到 0%。我不习惯把测试服务一直挂在机器上,更不会用模糊的批量终止命令,以免影响同机上的其他进程。 下一次我准备直接替换更大的 Qwen Coder,保持这套接口和并发脚本不变。到那时,最值得比较的不会只有 token/s,还包括代码修复通过率、单测成功率、首 token 延迟和单位显存能承载的并发。
全部评论(1)
只看楼主
潜水1号
1楼
nb
9/28
没有更多内容了
--
楼主
Level.1
0 帖子
楼主热帖
暂无数据