# 海光DCU大模型推理生产落地:从环境搭建到稳定运维的完整实战笔记
国产算力落地到生产环境,从来不是“把模型跑起来”这么简单。很多团队都遇到过类似的问题:实验室里单卡跑demo一切正常,一到线上高并发场景就性能跳水、显存泄漏、服务偶发崩溃,排查起来又缺乏成熟的方法论,只能靠反复试错踩坑。
事实上,海光DCU的推理落地是一套系统性工程:环境基线决定了性能的下限,部署选型决定了资源利用率的天花板,调优策略决定了最终的吞吐表现,运维体系则保障了长期运行的稳定性。四个环节环环相扣,任何一处短板都会拉低整体效果。本文结合多个信创项目的落地经验,把从环境搭建到生产运维的全流程实操方法整理出来,每一步附带可复用的脚本与参数建议,帮助团队少走弯路,快速实现从“跑通”到“用好”的跨越。
## 一、环境打底:三类隐性配置,决定性能下限
很多人容易忽略环境层的重要性,觉得“能识别设备、能启动模型”就算环境合格。实际上至少有三成的性能损耗,都来自环境配置的隐性问题——版本不匹配、路径优先级错误、NUMA跨节点调度,这些问题不会让程序报错,却会悄悄吃掉20%~40%的硬件性能。
### 1. DTK版本选型:不是越新越好
DTK作为DCU的核心工具链,版本选择的第一原则是“匹配优先,稳定优先”,而不是盲目追新。内核驱动、DTK、PyTorch三者必须版本对齐,任何一环错位都会出现“能启动但性能异常”的诡异问题。
- 生产环境优先选择经过厂商验证的稳定版:比如深算二号K100优先搭配DTK 26.04,深算一号Z100优先搭配DTK 25.04.4,这两个版本的算子适配、工具链完整度都经过了大规模场景验证;
- 版本一致性校验要做双端:既要查用户态DTK版本,也要查内核驱动版本,两者大版本必须对应。
```bash
# 查用户态DTK版本
hipcc --version | grep "HIP version"
# 查内核驱动版本
lsmod | grep hydcu
modinfo hydcu | grep version
```
新手最容易踩的坑是:系统里预装了多个版本DTK,环境变量配置混乱,编译时用一个版本,运行时加载另一个版本的动态库,最终出现算子不兼容、性能骤降的问题。更稳妥的做法是在作业脚本开头显式加载指定版本,避免依赖系统默认环境。
### 2. PyTorch安装:版本对应是第一原则
PyTorch的安装是高频踩坑点,直接 `pip install torch`默认下载CUDA版本,在DCU环境里永远识别不到设备。正确的安装逻辑是:先确认DTK对应的ROCm版本,再安装对应ROCm版本的PyTorch。
版本对应关系可以简单记为:DTK 25.x对应ROCm 5.x,DTK 26.x对应ROCm 6.x。以DTK 26.04为例,标准安装命令如下:
```bash
pip install torch==2.4.0 torchvision==0.19.0 \
--index-url https://download.pytorch.org/whl/rocm6.0
```
安装完成后不能只看 `torch.cuda.is_available()`返回True就完事,还要做两层校验:一是确认ROCm运行时版本正确,二是确认设备架构与硬件匹配。
```python
import torch
print(f"PyTorch版本: {torch.__version__}")
print(f"HIP运行时版本: {torch.version.hip}")
print(f"设备可用: {torch.cuda.is_available()}")
print(f"设备架构: {torch.cuda.get_device_properties(0).gcnArchName}")
```
如果架构名显示异常、或者HIP版本和DTK不对应,后续运行大概率会出现数值错误或者性能异常,越早排查越省时间。
### 3. NUMA亲和性:零成本的性能提升
主流双路服务器的DCU通常分属两个NUMA节点,组内卡间通过xGMI高速互联,跨组通信则要经过CPU总线,带宽只有组内的三分之一左右。如果进程不做绑核,操作系统会随机调度CPU核心,频繁跨NUMA传输数据,多卡并行效率可能连60%都到不了。
优化方法非常简单:启动任务前先查清楚显卡对应的NUMA节点,用 `numactl`把进程绑定到对应NUMA节点的CPU和内存上,完全零代码改动,通常能带来20%~40%的性能提升。
```bash
# 第一步:查看DCU拓扑与NUMA归属
rocm-smi --showtopo
# 第二步:绑定对应NUMA节点启动,示例:前4卡绑定NUMA 0
numactl --cpunodebind=0 --membind=0 \
python your_inference_script.py
```
单卡推理场景下,绑核的收益可能不明显,但在4卡、8卡多卡张量并行的场景下,这个优化的效果会非常显著,是所有优化里投入产出比最高的一项。
## 二、部署选型:模型、精度与框架的匹配逻辑
环境配置合格之后,下一步是选择合适的部署方案。同样的硬件和模型,不同的精度、框架、参数配置,最终的吞吐和延迟可能差出数倍。选型没有绝对的最优解,核心是在业务的精度要求、延迟要求、并发需求之间找平衡点。
### 1. 卡型与模型规格的匹配参考
不同量级的模型,对显存和算力的需求差异很大,选对配置才能兼顾成本和性能。以下是生产环境的常用匹配参考,基于BF16精度、vLLM框架:
- **7B及以下模型**:单张K100(96GB显存)即可轻松承载,剩余显存可以支撑大量KV缓存,支持高并发请求;即使是Z100(64GB显存)也能单卡部署,适合边缘场景、低并发业务。
- **32B~70B模型**:需要4~8张K100做张量并行,推荐8卡整机部署,xGMI全互联能保证多卡通信效率;如果是Z100机型,通常需要8卡以上才能承载70B级模型,且并发能力有限。
- **多模态模型**:视觉塔会额外占用一部分显存和算力,同参数模型建议按提升一个量级配置硬件,比如7B级多模态模型建议预留至少24GB显存余量。
### 2. 精度选择:在性能、显存、精度之间做取舍
四种常见精度各有适用场景,不是精度越低越好,也不是越高越稳妥:
- **FP32**:精度最高但显存占用大、速度慢,除了训练梯度计算场景,推理基本不会使用;
- **FP16/BF16**:推理场景的主流选择,显存占用是FP32的一半,精度损失可忽略,DCU硬件原生支持,兼容性最好,业务对精度有要求时优先选这个;
- **FP8**:显存占用再减半,吞吐提升明显,但有两个注意点:一是DCU的FP8编码格式和NVIDIA不通用,不能直接复用CUDA环境的量化权重;二是部分复杂算子支持度一般,适合以自回归解码为主的纯推理场景。
- **INT8**:显存占用最低,但精度损失相对明显,适合对响应速度要求高、精度容忍度高的场景,比如检索、分类类任务。
### 3. vLLM生产级参数详解
vLLM是目前DCU上推理性能最优的框架之一,但很多人直接照搬CUDA的启动参数,导致稳定性和性能都不达预期。DCU环境有几个专属参数需要重点调整,下面是生产环境的标准启动模板,附带每个参数的设计逻辑:
```bash
#!/bin/bash
source /opt/dtk-26.04/env.sh
export TRITON_HIP_LLD_PATH=/opt/dtk-26.04/llvm/bin/ld.lld
numactl --cpunodebind=0 --membind=0 \
vllm serve /data/models/Qwen2-7B-Instruct \
--served-model-name Qwen2-7B-Instruct \
--tensor-parallel-size 1 \
--dtype bfloat16 \
--max-model-len 8192 \
--gpu-memory-utilization 0.9 \
--kv-cache-dtype fp8 \
--enforce-eager \
--max-num-seqs 256 \
--host 0.0.0.0 \
--port 8000
```
几个关键参数的选型逻辑:
- `--gpu-memory-utilization 0.9`:预留10%显存给运行时开销、临时张量,避免高并发下触发OOM;不要设成0.95以上,DCU运行时的显存开销比CUDA略高,打满显存很容易崩溃。
- `--enforce-eager`:关闭CUDA Graph,这是DCU环境的必加参数。CUDA Graph在部分算子上兼容度不佳,会导致偶发崩溃和长尾延迟飙升,开启eager模式后虽然单序列速度略有下降,但P99延迟和稳定性会大幅提升。
- `--kv-cache-dtype fp8`:开启FP8 KV缓存,显存占用直接减半,能支撑的并发数几乎翻倍,是性价比最高的优化项之一,且对最终生成质量影响极小。
- `--max-num-seqs 256`:最大并发序列数,这个值不是越大越好。太小会导致算力喂不饱,太大则会让每个请求的延迟升高,需要根据业务的吞吐和延迟要求平衡。
## 三、性能调优:三层优化路径,按需落地
性能优化不用上来就写自定义算子,可以按照从易到难的顺序分层落地:先做零代码的通用优化,再做框架级参数调优,最后针对热点算子做深度定制。大部分场景下,做完前两层就能达到80分的性能,足够满足生产需求。
### 第一层:通用基础优化(零代码改动)
这一层不需要改业务代码,只需要调整启动配置和环境参数,就能覆盖大部分性能问题,适合所有场景优先落地。
除了前面提到的NUMA绑核、开启eager模式、FP8 KV缓存之外,还有两个实用技巧:
1. **固定显存分配阈值**:避免运行时频繁申请释放显存,减少调度开销。可以通过环境变量设置PyTorch缓存分配器的行为,减少显存碎片。
2. **避开显存残留卡**:DCU的显存回收机制和CUDA不同,进程异常退出后显存不会立刻释放,新任务尽量选择空闲卡启动,通过 `HIP_VISIBLE_DEVICES`指定设备编号,避免和残留进程抢显存。
### 第二层:算子级融合优化(轻量代码改动)
如果基础优化后性能仍有瓶颈,可以针对高频算子做融合优化,核心思路是减少中间张量的读写,把多次显存访问合并成一次。
最典型的就是RMSNorm、Bias+GELU这类逐元素算子,原生PyTorch实现会产生多次显存读写,融合成单个核函数后性能能提升1.5倍左右。下面是一个简化的融合Bias+GELU核函数示例:
```cpp
__global__ void fused_bias_gelu_kernel(const float* input, const float* bias,
float* output, int rows, int hidden) {
int row = blockIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
if (row >= rows || col >= hidden) return;
int idx = row * hidden + col;
float val = input[idx] + bias[col];
constexpr float kSqrt2OverPi = 0.79788456f;
constexpr float kCoeff = 0.044715f;
float cubic = val * val * val;
output[idx] = 0.5f * val * (1.0f + tanhf(kSqrt2OverPi * (val + kCoeff * cubic)));
}
```
这类融合算子的开发成本不高,但收益非常明显,尤其适合Transformer里的高频小算子。如果团队有HIP开发能力,优先优化Top5热点算子,整体性能就能上一个台阶。
### 第三层:吞吐与延迟的平衡调优
当服务进入稳定运行阶段,就需要根据业务特征做精细化调优。核心是在吞吐和延迟之间找平衡点:
- **高吞吐优先场景**:比如离线批量推理、非实时问答,适当调大 `max-num-seqs`,增加批处理大小,让硬件尽量跑满,追求单位时间处理的请求总数最大化;
- **低延迟优先场景**:比如实时对话、在线客服,适当减小最大并发数,避免请求排队,同时可以调小 `max-model-len`,缩短单次推理的计算量;
- **长文本场景**:重点优化KV缓存的分页效率,开启前缀缓存复用,减少长上下文的重复计算。
## 四、生产运维:稳定性与可观测性建设
生产环境和实验室demo最大的区别,就是需要保证7×24小时稳定运行。很多团队只关注上线时的性能,却忽略了运维体系建设,结果线上频繁出问题,排查又没有抓手。其实只要做好三层保障,就能大幅降低故障率。
### 1. 日常健康巡检:把问题扼杀在萌芽期
每天做一次基础巡检,提前发现潜在风险,比故障发生后再排查效率高得多。下面是一个通用巡检脚本,覆盖硬件状态、服务状态、资源占用三大类核心指标:
```bash
#!/bin/bash
echo "=== DCU 推理服务日常巡检 $(date '+%Y-%m-%d %H:%M') ==="
echo -e "\n1. 硬件状态检查"
rocm-smi --showuse --showmemuse --showtemp --csv 2>/dev/null | tail -n +2 | \
awk -F',' '{printf " 卡%s: 使用率%s%%, 显存%s/%s, 温度%s℃\n", $1, $3, $5, $6, $8}'
echo -e "\n2. 推理服务状态"
if [ -f vllm.pid ] && kill -0 "$(cat vllm.pid)" 2>/dev/null; then
echo " 服务运行中,PID: $(cat vllm.pid)"
curl -s http://localhost:8000/v1/models >/dev/null 2>&1 && echo " 接口响应正常" || echo " 接口无响应"
else
echo " 服务未运行"
fi
echo -e "\n3. 显存与进程检查"
count=0
for dev in /dev/dri/renderD*; do
pids=$(fuser "$dev" 2>/dev/null)
if [ -n "$pids" ]; then
count=$((count + $(echo "$pids" | wc -w)))
fi
done
echo " 共 $count 个进程占用DCU设备"
echo -e "\n✅ 巡检完成"
```
通过巡检可以提前发现显存异常占用、温度过高、服务无响应等问题,及时处理避免扩大成线上故障。
### 2. 常见故障的排查流程
三类最常见的线上故障,按优先级排查基本都能快速定位:
1. **服务OOM崩溃**:优先检查是否并发过高、KV缓存设置过大,再排查是否有显存泄漏,最后确认是否有其他进程抢占显存;
2. **性能突然下降**:先查是否有其他任务抢占资源,再查NUMA绑定是否失效、是否切换了运行设备,最后核对模型权重、精度是否被改动;
3. **偶发返回异常**:优先排查算子兼容性问题,尤其是自定义算子、量化算子,再检查是否有静默数值错误,必要时关闭对应优化项回退到稳定路径。
### 3. 简单的服务自愈机制
生产环境不可能时刻有人值守,给服务加一层简单的守护和自愈能力,能大幅降低运维压力。最基础的方式是写一个定时检测脚本,发现服务异常时自动重启:
```bash
#!/bin/bash
# 简单的服务健康检查与自愈脚本
LOG_FILE="./service_guard.log"
if ! curl -s http://localhost:8000/v1/models >/dev/null 2>&1; then
echo "$(date '+%Y-%m-%d %H:%M:%S') 服务无响应,执行重启" >> "$LOG_FILE"
bash vllm_service.sh stop
sleep 5
bash vllm_service.sh start
echo "$(date '+%Y-%m-%d %H:%M:%S') 服务重启完成" >> "$LOG_FILE"
fi
```
配合crontab定时执行,就能实现基础的故障自愈,应对偶发的服务崩溃完全够用。
## 写在最后
海光DCU的推理落地,本质上是一个工程化问题。硬件参数决定了理论上限,但最终能发挥出多少性能,全看环境、部署、调优、运维每一个环节的打磨程度。
不用一开始就追求极致性能,也不用上来就做深度算子开发。先把环境基线对齐,把部署配置选对,把基础优化落地,再根据业务瓶颈逐步深入,大部分场景下都能获得非常不错的效果。国产算力的生态还在快速成熟,工程经验越沉淀,后续的落地成本就越低,长期收益也会越明显。