Datawhale x AMD Hello-ROCm + Qwen2.5-3B-Instruct

云端推理部署实战

1. Hello-前言:为什么要在 48GB AMD 显卡上跑第二个模型?

事情的起因是这样的:我在云上有一台搭载 AMD GPU 的实例,上面已经跑着一个 Gemma 4 E4B 模型的 LoRA 情绪识别微调项目。GPU 显存总共 48GB,Gemma 4 占了约 32.7GB(68%),还剩大约 15GB 空闲。

手里有闲着的显存,就像口袋里有多余的现金——不花出去总觉得亏。于是决定在同一个实例上再部署一个轻量级大模型,跟 Gemma 4 搭伙干活。

1.1 选型:四个候选,一个答案

剩余约 15GB 显存的硬约束,直接把 7B+ 的模型排除在外。在可使用范围内筛选了四个候选:

模型 FP16 大小 特点
Qwen2.5-1.5B ~3GB 中文能力一流,阿里出品
Qwen2.5-3B ~6GB 中文+代码双优,32K 上下文
Gemma 3-1B ~2GB 极轻量,但跟已有 Gemma 4 重合
Llama 3.2-3B ~6GB 英文强、生态好,中文一般

结论毫无悬念:选 Qwen2.5-3B-Instruct。原因很简单——我手上的业务场景(金融文档、教育 AI)都是中文密集型的,Qwen 在中文理解和生成上的优势是另外三个无法比拟的。6GB 的显存开销也完全在预算之内。

1.2 环境信息

GPU:     AMD MI300 系列 (Device ID 0x744b)
VRAM:    48 GB HBM (rocm-smi 实测)
系统:    Ubuntu + ROCm 7.x
Python:  3.12.3
关键包:  transformers 5.x, vLLM 0.23.0, PyTorch 2.10

2. 环境准备与模型下载

2.1 检查 GPU 状态

动手之前,先用 rocm-smi 摸清家底:

$ rocm-smi

================================================== Concise Info ==================================================
Device Node IDs Temp Power Partitions SCLK MCLK Fan Perf PwrCap VRAM% GPU%
 (DID, GUID) (Edge) (Avg) (Mem, Compute, ID)
==================================================================================================================
0 5 0x744b, 13037 25.0°C 10.0W N/A, N/A, 0 0Mhz 96Mhz 20.0% auto 241.0W 68% 0%
==================================================================================================================

$ rocm-smi --showmeminfo vram
GPU[0] : VRAM Total Memory (B): 51522830336    # ≈ 48 GB
GPU[0] : VRAM Total Used Memory (B): 35168178176  # ≈ 32.7 GB (68%)

温度 25°C、功耗 10W、GPU 利用率 0%——显卡在摸鱼,正是搞事情的好时机。

2.2 下载 Qwen2.5-3B-Instruct

使用魔搭 ModelScope 下载模型,完全脱离 HuggingFace Hub(无需 HF_TOKEN,私有化友好):

from modelscope import snapshot_download

model_dir = snapshot_download(
    'Qwen/Qwen2.5-3B-Instruct',
    cache_dir='/workspace/repo/src/models'
)
print(f'下载完成: {model_dir}')

几分钟就下完了。模型文件落地路径:

/workspace/repo/src/models/Qwen/Qwen2.5-3B-Instruct/

值得注意的是,ModelScope 在下载完成后会在模型目录内创建一个 ._____temp 临时目录和 .lock 锁文件,这是正常的下载残留,不影响使用。


3. vLLM 部署尝试(踩坑记录)

按照"标准操作流程",第一反应是上 vLLM。毕竟 vLLM 是目前开源社区最主流的 LLM 推理引擎,PagedAttention、Continuous Batching 这些特性耳熟能详。

3.1 第一次尝试:相对路径陷阱

$ vllm serve ./models/Qwen/Qwen2.5-3B-Instruct/ --served-model-name qwen2.5-3b

# 报错:
# OSError: Repo id must be in the form 'repo_name' or 'namespace/repo_name'

vLLM 不接受相对路径,必须用绝对路径。这算是个小坑——HuggingFace transformers 的 from_pretrained() 对相对路径宽容得多。

3.2 第二次尝试:v1 引擎在 ROCm 上崩溃

换成绝对路径后:

$ vllm serve /workspace/repo/src/models/Qwen/Qwen2.5-3B-Instruct/ \
  --served-model-name qwen2.5-3b

# 报错:
# RuntimeError: Engine core initialization failed.
# See root cause above. Failed core proc(s): {}

嗯?没有 root cause?往上翻日志——一堆 vLLM v1 engine 的内部调用栈,没有具体的硬件兼容性报错,就是默默地崩溃了。

3.3 第三次尝试:强制降级 v0 引擎

vLLM 从 v1 架构开始大幅重构了引擎核心,换成环境变量 VLLM_USE_V1=0 强制用老版本引擎:

$ VLLM_USE_V1=0 vllm serve \
  /workspace/repo/src/models/Qwen/Qwen2.5-3B-Instruct/ \
  --served-model-name qwen2.5-3b \
  --max-model-len 4096

# 同样的 RuntimeError,完全相同的错误

有意思——VLLM_USE_V1=0 环境变量没有被吃掉(日志里看不到警告说它被忽略),但引擎还是在 v1 路径上崩溃了。

3.4 Root Cause 分析

结合日志和社区反馈,这个问题的根因可以锁定:

  • vLLM 的 v1 引擎引入了新的进程管理和初始化体系,AMD ROCm 下的适配尚不成熟
  • VLLM_USE_V1=0 在当前版本(v0.23.0+rocm723)可能已失效——上游正在逐步废弃 v0 引擎
  • 引擎核心进程在启动时静默崩溃,日志不够友好:Failed core proc(s): {} 后面是空字典——本应包含失败进程 PID 的

3.5 放弃 vLLM 的决策逻辑

坦率地说,对于 Qwen2.5-3B 这种 30 亿参数的小模型,强行上 vLLM 本身就是"杀鸡用牛刀"。

vLLM 的核心价值在于:PagedAttention 的高吞吐 KV Cache 管理、Continuous Batching 的多请求合并、Tensor Parallelism 的多卡分布式推理。而 3B 模型:

  • 单卡显存完全够用(6GB / 48GB),不需要分布式
  • 个人使用的并发请求量极少(一般就 1-2 个),Continuous Batching 的收益接近于零
  • 模型小到不需要 PagedAttention 做显存优化——标准 attention 就够了

结论:与其花半天时间跟 vLLM 的 ROCm 兼容性较劲,不如直接用 transformers 加载模型做推理。简单、稳定、够用。


4. Transformers 推理验证与常驻服务

4.1 快速推理验证

用一段最简代码先跑通推理:

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

model_path = '/workspace/repo/src/models/Qwen/Qwen2.5-3B-Instruct'
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
    model_path, dtype=torch.float16, device_map='auto',
    trust_remote_code=True
)

msg = [{'role':'user','content':'你好,请用一句话介绍自己'}]
text = tokenizer.apply_chat_template(msg, tokenize=False, add_generation_prompt=True)
inputs = tokenizer(text, return_tensors='pt').to(model.device)
out = model.generate(**inputs, max_new_tokens=50, pad_token_id=tokenizer.eos_token_id)
print(tokenizer.decode(out[0][len(inputs['input_ids'][0]):], skip_special_tokens=True))

输出:

✅ 我是Qwen,由阿里云开发的超大规模语言模型,旨在提供帮助和解答各种问题。

一次跑通,没毛病。加载速度极快(430+ it/s),3B 的轻量优势体现得淋漓尽致。

4.2 常驻推理服务的兼容性挑战

验证通过后,要写一个常驻推理脚本让模型一直保持在显存里。这里遇到了本次部署最大的坑——transformers 5.x 的 API 兼容性问题

4.2.1 apply_chat_template 返回值变了

在 transformers 4.x 中,apply_chat_template(tokenize=False) 返回的是一个纯字符串,直接可以喂给 tokenizer。但 5.x 版本的行为发生了变化——同样的参数可能返回 BatchEncoding 对象或列表,导致后续的 tokenizer() 调用失败:

# 报错:TypeError: TextEncodeInput must be Union[TextInputSequence, ...]
# 原因:apply_chat_template 返回了列表而不是字符串
4.2.2 BatchEncoding 的 .shape 陷阱

第二个坑:transformers 5.x 的 apply_chat_template(tokenize=True) 返回 BatchEncoding 对象后,直接访问 .shape 属性会抛出 AttributeError

inputs = tokenizer.apply_chat_template(messages, tokenize=True, ...)
input_len = inputs.shape[1]  # ❌ AttributeError: 'BatchEncoding' has no 'shape'

# 必须先取 input_ids
input_len = inputs['input_ids'].shape[1]  # ✅
4.2.3 最终解决方案:绕过 apply_chat_template

经历多轮迭代修复后,终极方案是——干脆不用 apply_chat_template。回到最底层的 API:手动构造 ChatML 格式的字符串,然后用最简单的 tokenizer(text) 生成 input_ids。这是 transformers 至今所有版本都兼容的方式:

def chat(prompt):
    # 直接用 Qwen 的 ChatML 格式模板
    formatted = (
        "<|im_start|>system\n"
        "You are a helpful assistant.<|im_end|>\n"
        "<|im_start|>user\n"
        + prompt +
        "<|im_end|>\n"
        "<|im_start|>assistant\n"
    )
    inputs = tokenizer(formatted, return_tensors="pt").to(model.device)
    input_len = inputs["input_ids"].shape[1]
    with torch.no_grad():
        outputs = model.generate(
            input_ids=inputs["input_ids"],
            attention_mask=inputs.get("attention_mask"),
            max_new_tokens=512,
            temperature=0.7,
            do_sample=True,
            pad_token_id=tokenizer.eos_token_id,
        )
    return tokenizer.decode(
        outputs[0][input_len:], skip_special_tokens=True
    ).strip()

这一版经过了实际推理测试的验证,工作正常:

>>> 你好,你是谁?
你好!我是来自阿里云的大规模语言模型,我叫通义千问。

>>> 郑州属于中国的哪个省?
郑州位于中国河南省。

>>> 如何评估开源大模型的安全性?
评估开源大模型的安全性可以从多维度进行...(完整专业回答)

三条测试全部通过,没有报错。

4.2.4 最终常驻服务脚本
# 文件: /workspace/repo/src/scripts/qwen_server.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL_PATH = "/workspace/repo/src/models/Qwen/Qwen2.5-3B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
    MODEL_PATH, dtype=torch.float16, device_map="auto", trust_remote_code=True
)

def chat(prompt):
    prompt = prompt.strip().replace("\n", " ")
    formatted = f"<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"
    inputs = tokenizer(formatted, return_tensors="pt").to(model.device)
    input_len = inputs["input_ids"].shape[1]
    with torch.no_grad():
        outputs = model.generate(
            input_ids=inputs["input_ids"],
            attention_mask=inputs.get("attention_mask"),
            max_new_tokens=512, temperature=0.7, do_sample=True,
            pad_token_id=tokenizer.eos_token_id,
        )
    return tokenizer.decode(outputs[0][input_len:], skip_special_tokens=True).strip()

while True:
    user_input = input(">>> ").strip()
    if user_input.lower() in ("quit", "exit", "q"):
        break
    if not user_input:
        continue
    print("\n" + chat(user_input) + "\n")

运行命令:

python3 /workspace/repo/src/scripts/qwen_server.py

5. 双模型共存的显存管理

5.1 部署前后对比

部署前后跑 rocm-smi 做对比:

  • 部署前(只有 Gemma 4 E4B):VRAM%: 68% | GPU%: 0%
  • 部署后(Gemma 4 + Qwen 2.5-3B 同时加载):VRAM%: 68% | GPU%: 0%

等等……显存没涨?

对,因为前面跑推理验证的 Python 进程退出后,Qwen 模型就从显存里释放了。这才是关键点:

  • Python 进程退出 = 显存释放。GPU 不像硬盘,不会"记住"加载过的模型。
  • 两个模型不能通过先后运行两个脚本来"共存"——必须在一个进程里同时加载。
  • 常驻推理服务的意义就在于此:保持进程不退出,模型就一直在显存里待命。

5.2 双模型共存的可行性

理论上,Qwen 2.5-3B(~6GB)和 Gemma 4 E4B(~32.7GB)一共约 38.7GB,刚好塞进 48GB 的显存里。但需要在一个脚本中同时加载两个模型,管理各自的推理管线。这是下一步的工作方向。


6. 经验总结与私有化部署启示

6.1 三点核心教训

🔸 小模型不必上 vLLM
vLLM 是为高吞吐、多并发、分布式设计的重型引擎。3B 级别的小模型直接上 transformers 就是最好的选择。别为了"看起来专业"增加不必要的复杂度。

🔸 AMD ROCm 生态仍需"绕路走"
ROCm 下 vLLM v1 引擎的兼容性不如 CUDA 成熟。遇到引擎级别的崩溃,果断降级策略——从 vLLM 退到 transformers,从 v1 退到手动管理。完美主义在 ROCm 上最浪费时间。

🔸 版本兼容性比算法更耗时间
本次部署耗时最多的不是下载模型、不是写推理逻辑,而是处理 transformers 5.x 的 API 变更。高版本框架的 Breaking Change 是私有化部署中最容易踩的暗坑。

6.2 对私有化部署的启示

这次部署看似只是"再装一个模型",但实际上验证了几个对私有化场景很重要的结论:

  • ModelScope 的 snapshot_download 是比 git-lfs 更可靠的模型拉取方式——尤其是在国内网络环境下,腾讯云镜像的速度远优于 HuggingFace Hub
  • 绕过 apply_chat_template、回到最底层 tokenizer API 的做法,本质上是一种"最低共同特性"策略——选择所有版本都兼容的那一层接口,避免被框架升级牵着走
  • 48GB 显存的 AMD 实例完全能跑两个模型(一大一小),对于需要并行多模型推理的边缘部署场景是有参考价值的
  • 应建立一个显存和进程的显式管理体系——在私有化部署持续扩展时,需跟踪每个模型的显存占用和生命周期

7. FAQ

Q: 为什么不用 vLLM?
A: 不是不支持 vLLM,而是在 AMD ROCm 下的当前版本(v0.23.0)中,v1 引擎存在兼容性问题,且对 3B 小模型而言 vLLM 没有显著收益。用 transformers 更直接、更稳定。

Q: 为什么选 Qwen 而不是其他模型?
A: 业务需求以中文为主(金融文档、教育 AI),Qwen 的中文能力远胜 Llama 和 Gemma 3,且 3B 版本显存友好。选型原则是"场景适配 > 模型大小"。

Q: 怎么避免 transformers 版本兼容性问题?
A: 最稳妥的办法:回到 tokenizer(text) 这个底层 API。ChatML 格式模板虽然是手动拼接的,但在所有 transformers 版本下都能正常工作,不会因为 apply_chat_template 的行为变更而崩溃。

Q: 两个模型能同时跑吗?
A: 理论上可以。现在两个模型一共约 38.7GB(Gemma 4 的 32.7GB + Qwen 3B 的 6GB),48GB 显存装得下。但需要在一个脚本中管理两个模型的加载和推理,下一篇文章会展开讲。


8. 结语

这次部署的完整耗时分配很有意思:

  • 下载模型:3 分钟
  • 首次推理验证(transformers):1 分钟
  • vLLM 踩坑和放弃:5 分钟
  • 常驻服务脚本的兼容性调试:25 分钟

版本兼容性调试占了总时间的 70%+。 这就是私有化部署的真实写照:算法不难,环境最磨人。

最终的代码只有不到 40 行,但背后的"不做什么"比"做什么"更值得记录:不做 vLLM、不做多卡分布式、不做 apply_chat_template、不做过度工程化。**小模型的部署哲学应该是"用最简单的工具做最可靠的事"。

摘要

本文记录了在已部署 Gemma 4 E4B 模型的 48GB AMD GPU 实例上,额外部署 Qwen2.5-3B-Instruct 模型的完整实战过程。主要内容包括:

  1. 选型决策:基于剩余约15GB显存和中文密集型业务需求,选择 Qwen2.5-3B-Instruct(FP16约6GB)。

  2. vLLM 部署踩坑:尝试使用 vLLM 部署时,因 AMD ROCm 下 v1 引擎兼容性问题导致多次失败,最终放弃 vLLM 方案。

  3. Transformers 直接部署:改用 transformers 库直接加载模型,成功完成推理验证。但在编写常驻服务脚本时,遇到 transformers 5.x 版本 API 变更带来的兼容性问题,特别是 apply_chat_template 方法的行为变化。

  4. 最终解决方案:绕过 apply_chat_template,手动构造 ChatML 格式字符串,使用底层 tokenizer(text) API 实现稳定兼容的推理服务。

  5. 核心经验

    • 小模型(3B级别)无需复杂推理引擎,transformers 直接加载更简单可靠
    • AMD ROCm 生态仍需谨慎对待兼容性问题
    • 版本兼容性调试是私有化部署中最耗时的环节
    • 采用"最低共同特性"策略,选择所有版本都兼容的底层 API
  6. 显存管理:验证了在48GB显存中同时运行 Gemma 4(约32.7GB)和 Qwen 2.5-3B(约6GB)的可行性,为多模型并行推理提供了实践参考。

最终部署代码仅40行左右,体现了"用最简单的工具做最可靠的事"的小模型部署哲学。
**

更多推荐