☰
TensorRT-LLM部署Qwen1.5实战:从引擎编译到Triton服务化
2026/10/5 3:56:11 网站建设 项目流程

简介:面向大模型部署与推理优化工程师,提供基于TensorRT-LLM部署Qwen1.5大语言模型的完整项目源码与流程教程,清晰解决从模型权重转换到高性能推理引擎落地的关键难点。压缩包共5个文件,以4个Python脚本为核心(分别对应checkpoint转换、模型结构定义、层工具等)配合1个Markdown教程,整体仅25KB,代码精炼适合快速阅读与二次开发。已有592人学习,教程逐步讲解TensorRT-LLM环境配置、模型转换、推理引擎构建与性能验证,帮助读者在NVIDIA GPU上高效运行Qwen1.5,降低大模型部署门槛,是实战型参考案例。

1. 为什么选 TensorRT-LLM 部署 Qwen1.5:现场有 GPU,就别让算力睡大觉

做过企业大模型私有化部署的同学,大多有过这种体会:部署一个大语言模型最耗时的不是下载权重,而是把权重变成一台 GPU 服务器上稳定的在线服务。Qwen1.5 就是开源千问模型里很值得私有化落地的一代,7B/14B 在 4090 或 A10 这类卡上就能跑,Chat 版对话效果也不错。而按我这几年的落地经验,真正能把这代模型性能压出来的方案,还是 NVIDIA 的 TensorRT-LLM。和 Ollama 这类开箱即用但难抠细节、vLLM 部署方便但延迟和显存还有优化空间的工具相比,TensorRT-LLM 把模型编译成 TensorRT 引擎,首 token 延迟更低、吞吐明显更高,特别适合本地部署大模型时做低并发高 QPS 的在线服务。这篇文章就把完整流程拆开讲:怎么选卡、怎么转引擎、怎么部署服务,以及我在实际项目里踩过的坑。

2. 环境与权重准备:先把 TensorRT-LLM 和 Qwen1.5 在这一代卡上对齐

2.1 先算显存:7B、14B、72B 到底谁上你的机器

拿到 Qwen1.5 系列,第一件事不是 clone 代码,而是对着nvidia-smi看显存。Qwen1.5-7B 的 bf16 权重大约占 14GB,一张 24GB 的 4090 或 A10 其实能跑,但并发一高就会出现显存瓶颈;Qwen1.5-14B 的 bf16 权重约 28GB,一张 24GB 卡放不下,要么用两张卡做 TP2,要么做 INT8 量化把权重压到 14GB 左右再上单卡。72B 更不用说,建议至少 4 张 80GB 的 A100/H800,或者 8 卡做 TP8。这个判断直接决定你后面--tp_size、--max_batch_size和量化方案怎么写。

我一般先建一个粗略预算:每 10 亿参数在 bf16 下约占 2GB 权重,再留出 30% 显存给 KV cache 和激活。具体每个 token 的 KV cache 占用有公式可算,第 6 章会展开;这里你只需要明白,单卡能不能跑,看的不是模型“多大参数”,而是“权重 + 上下文 + batch”三个变量一起算。很多项目明明能跑 7B,却硬要去跑 14B,结果频繁 OOM,浪费一整天调试,这就是第一步没算清楚。

模型bf16 权重约24G 单卡双 24G TP2备注
Qwen1.5-7B14GB可跑,并发受限不必要4090 即可
Qwen1.5-14B28GB需 INT8/FP8推荐单卡 24G 放不下 bf16
Qwen1.5-72B144GB不可单卡不行至少 4 卡 80G

提示:显存不是唯一指标。4090 的 FP8 能力比 A100 好,A100 的显存带宽更高,这些差异会在量化章节里体现。

2.2 用官方容器而不是裸装:环境依赖一次对齐

TensorRT-LLM 对版本极其敏感:它和你机器上的 TensorRT、CUDA、cuDNN、PyTorch 深度绑定,任何一个小版本错位,编译出来的 engine 可能加载不了,或者运行时报出莫名其妙的 CUDA error。所以我的原则是:绝不裸装,一律用官方容器镜像进入开发环境。常见做法是拉取 NVIDIA Triton 的 TensorRT-LLM 容器,或者 GitHub Releases 里对应版本发布的 trtllm 镜像。以 24.05 这一批 tag 为例:

# 拉取与 TensorRT-LLM 配套的官方容器镜像 docker pull nvcr.io/nvidia/tritonserver:24.05-trtllm-python-py3 # 进入容器,并挂载工作目录 docker run --gpus all -it --shm-size=2g --ulimit memlock=-1 \ -v ${PWD}:/workspace \ nvcr.io/nvidia/tritonserver:24.05-trtllm-python-py3

两个容易忽略的参数:--shm-size=2g是因为 TensorRT engine build 时要用多进程做 plan 优化,共享内存太小会直接报unable to mmap;--ulimit memlock=-1是放开锁页内存限制,否则 Triton 在请求量大时可能无法分配 pinned memory。进入容器后,先跑python -c "import tensorrt_llm; print(tensorrt_llm.__version__)"确认版本,老版本 API 和新版本 API 差别很大,后面遇到问题第一先看版本。

另外别把宿主机上用 pip 装好的 TensorRT-LLM 硬塞进容器,不同版本的 ABI 不兼容。你要么用容器内的 Python,要么用相同版本在宿主机重新编译。我见过很多人在这一步因为图省事而翻车,花掉的排错时间足够重装两次环境。

2.3 获取 Qwen1.5 权重:文件名一个都不能少

权重从 Hugging Face 拉取,推荐用huggingface-cli而不是git lfs clone,前者有断点续传,也更方便内网转移:

# 优先用 huggingface-cli,支持断点续传 pip install -U huggingface_hub huggingface-cli download Qwen/Qwen1.5-7B-Chat \ --local-dir ./Qwen1.5-7B-Chat \ --local-dir-use-symlinks False

下载完成后,检查config.json、tokenizer.json、tokenizer_config.json、generation_config.json、model-00001-of-0000X.safetensors以及索引文件model.safetensors.index.json是否都在。很多人只复制权重文件,不复制 tokenizer,后面得到乱码输出,回头查才发现 tokenizer 没带。注意 Qwen1.5 和 Qwen2 的 tokenizer 并不相同,不能用另一套来充数。

如果你的服务器没有外网,可以在有网环境跑完huggingface-cli download,再把整个目录打包 tar 拷贝到内网。拷贝时保留目录结构,不要只拷*.safetensors。这个权重目录后面会同时给convert_checkpoint.py和加载 engine 时的 tokenizer 使用,路径写错或者文件缺失,都会在转换阶段报KeyError或tokenizer_config.json not found。下面章节我统一把它挂载到/workspace/Qwen1.5-7B-Chat。

2.4 快速验证环境的三条命令

在开始转换之前,用三条命令做一次环境体检,能省掉后面一半的报错。第一条是nvidia-smi看驱动是否支持需要的 CUDA 版本;第二条是容器内nvcc --version确认 CUDA;第三条是python -c "import tensorrt_llm"。我习惯把这三条一次性写到项目 README 或scripts/check_env.sh里。像标题这种带源码包和流程教程的实战项目,一般都会放一个初始化脚本,但无论脚本多全,你都要自己过一遍——因为容器 tag 换成别的版本,环境就变了。如果import tensorrt_llm直接失败,换个 tag 重新拉镜像,不要在裸环境里补包,这是最干净的重试路径。

3. 从权重到引擎:convert_checkpoint 与 trtllm-build 的完整命令

3.1 转换 checkpoint:为什么是 bfloat16

TensorRT-LLM 不能直接吃 Hugging Face 的 safetensors,它需要先把权重转成自己的 checkpoint 格式,再编译成 TensorRT engine。转换脚本在仓库的examples/qwen目录下:

# 转成 TensorRT-LLM checkpoint,tp_size 必须与后续 build 一致 cd TensorRT-LLM python examples/qwen/convert_checkpoint.py \ --model_dir /workspace/Qwen1.5-7B-Chat \ --output_dir /workspace/qwen_ckpt \ --dtype bfloat16 \ --tp_size 1

这段命令的逻辑是:读取原始模型目录,把权重分片并按张量并行(TP)方式重排,输出到qwen_ckpt。--tp_size必须和后面trtllm-build时的并行数一致,这里写 2 后面写 1,engine 加载会直接报权重 shape 不匹配。--dtype我强烈建议用bfloat16,除非你是 V100 这种不支持 bf16 的老卡;bf16 的指数位更宽,在长序列场景下不容易溢出,FP16 虽然占一样显存但偶尔会把 loss 跑飞。

如果转换脚本找不到 Qwen1.5 的模型定义,先确认 TensorRT-LLM 版本是否包含qwen示例,以及config.json里的model_type。Qwen1.5 的架构走的是 Qwen2 的代码路径,model_type通常是qwen2,TensorRT-LLM 会把它映射到QWenForCausalLM。有些老版本没有这个映射,报KeyError: 'qwen2'。这时候不要自己魔改代码,优先升级仓库到明确支持 Qwen1.5/Qwen2 的版本。

3.2 构建 TensorRT 引擎:几个必调参数

转换完 checkpoint,用trtllm-build编译引擎。新版统一命令是trtllm-build,老版本是build.py,两者参数基本对应,以 7B 单卡为例:

# 核心 engine 编译参数 trtllm-build \ --checkpoint_dir /workspace/qwen_ckpt \ --output_dir /workspace/qwen_engine \ --gemm_plugin bfloat16 \ --max_batch_size 8 \ --max_input_len 2048 \ --max_seq_len 8192 \ --max_num_tokens 4096 \ --log_level info

逐项说清楚。--gemm_plugin bfloat16让 GEMM 走插件实现,bf16 dtype 下才能拿到 TensorRT 的融合优化,不填的话很多融合 kernel 不生效,性能会有肉眼可见的下降。--max_batch_size是一个 engine 允许的并发序列数,不是同时并发请求数——inflight batching 会动态把 token 填进 batch,真正的限制实际由--max_num_tokens决定。--max_input_len限制用户输入的 token 长度,--max_seq_len是整个序列的硬上限(输入+输出),Qwen1.5 原生支持 32768 上下文,但我不建议首次 build 就拉满:序列长度每翻倍,KV cache 显存和 build 耗时都跟着涨,先跑通 8192,再根据业务加长。

--max_num_tokens是很多人容易忽略的参数。它控制 inflight batching 每一轮最多容纳的 token 总数,可以粗略理解为一个显存预算:等于 batch 内所有序列的当前长度之和。把这个值调小,能显著降低编译时间和峰值显存,但会压小 batch;调大,吞吐高但显存吃紧。7B 单卡从 4096 起步,14B 建议 2048 起步,之后再观察显存占用微调。

3.3 最小可运行配置:一张 4090 跑起 Qwen1.5-7B

下面是一份我用 4090 跑通的最小配置方案,可以直接抄:

参数值理由
dtypebfloat164090 原生支持 bf16 计算
max_batch_size8单卡 inflight batching 的合理起点
max_input_len2048覆盖大多数问答和文档摘要
max_seq_len8192上下文够用,KV cache 不失控
max_num_tokens4096编译快,显存占用可控

如果编译过程中出现Out of Memory During Build,优先把--max_num_tokens降到 2048,再把--max_batch_size降到 4。仍然不行,检查是不是容器内同时建了多个 engine 进程占显存。--log_level info是为了看到Total per token memory和Total activation memory,这两行数字能帮你判断剩余显存够不够跑。编译成功后会得到qwen_engine/config.json和一堆*.engine分片文件,我建议立刻cp -r qwen_engine /workspace/qwen_engine_v1备份一份——后面每次调参都重新 build,旧引擎留着当后悔药。

4. 跑推理与服务化:从离线批处理到 Triton 端到端

4.1 先离线推理,再谈服务化

拿到 engine,先不要急着上 Triton,先用 Python API 验证推理结果是否正常。TensorRT-LLM 的 API 有两代,新版本推荐用高级封装LLM,代码简单很多:

# 高版本 TensorRT-LLM 的统一入口 from tensorrt_llm import LLM, SamplingParams llm = LLM(engine_dir="/workspace/qwen_engine") params = SamplingParams(max_tokens=256, temperature=0.7, top_p=0.9) texts = [ "请用一句话介绍 Qwen1.5", "写一段 100 字的请假理由", ] outputs = llm.generate(texts, params) for out in outputs: print(out.outputs[0].text)

这段代码里LLM(engine_dir=...)负责加载 engine 和分配显存;SamplingParams控制生成参数;generate接受 list 自动做 batch,避免手写调度循环。如果你的容器版本比较老没有LLM类,可以用ModelRunner手动写:先读config.json,再ModelRunner.from_dir(engine_dir=...),生成时传max_new_tokens。两者核心逻辑一样。

还有一个关键细节:加载引擎时要显式指定原模型目录,而不是让 TensorRT-LLM 从 engine 目录猜 tokenizer:

# 注意:tokenizer 目录必须指向原始权重目录 llm = LLM(engine_dir="/workspace/qwen_engine", tokenizer_dir="/workspace/Qwen1.5-7B-Chat")

这样能避开一个非常隐蔽的坑:engine 目录里没有 tokenizer 文件,运行时如果路径找不到,TensorRT-LLM 可能用了无关 tokenizer,输出立刻乱码。这个问题在下一章避坑里还会出现。

4.2 用 Triton Inference Server 做成 HTTP 服务

离线验证通过后,企业私有化部署十有八九要用 Triton 统一管理多个模型。Triton 的tensorrt_llm后端内置 inflight batching,比自己写 FastAPI 再去并发请求 engine 省心得多。常见做法是参考官方仓库里的inflight_batcher_llm示例搭一个最小 model repo:

# 最小 Triton model repo 结构 model_repo/ └── qwen1_5/ ├── 1/ └── config.pbtxt

把/workspace/qwen_engine整个目录放在1/下,然后在config.pbtxt里写模型声明。一个能跑起来的简化配置如下:

# 模型名和 backend 必须与目录名一致 name: "qwen1_5" backend: "tensorrt_llm" max_batch_size: 8 input [ { name: "text_input" data_type: TYPE_STRING dims: [1] } ] output [ { name: "text_output" data_type: TYPE_STRING dims: [1] } ] parameters { key: "tensorrt_llm_model_path" value: { string_value: "/models/qwen1_5/1/qwen_engine" } } parameters { key: "max_tokens" value: { string_value: "512" } }

启动命令:

# 启动 Triton 服务,只加载 qwen1_5 这一个模型 docker run --gpus all -it --shm-size=4g \ -v ${PWD}/model_repo:/models \ -p 8000:8000 -p 8001:8001 -p 8002:8002 \ nvcr.io/nvidia/tritonserver:24.05-trtllm-python-py3 \ tritonserver --model-repository /models --model-control-mode explicit --load-model qwen1_5

启动时我用--model-control-mode explicit,只加载声明过的模型;如果不加,Triton 会尝试扫描加载 repo 里所有目录,遇到不完整的模型目录会把服务拉崩。看到"qwen1_5" is ready for requests日志后,再用 curl 或 Python 请求验证接口。实际项目中,我还会再封装一层 Python 进程,把请求转成 token id 再进 Triton,这样能拿到 streaming、停止词、长度限制这些更细的控制。

4.3 用 perf_analyzer 验证性能,而不是靠掐表

服务起来后,不要靠浏览器点一下、数秒来评估性能。Triton 自带的perf_analyzer是业界普遍认可的打点工具,它能度量并发、吞吐和延迟:

# 分别测 1/3/5/7 并发,每个点测 10 秒 perf_analyzer -m qwen1_5 -u localhost:8001 \ --concurrency-range 1:8:2 \ --input-data ./perf_input.json \ --measurement-interval 10000 \ --max-threads 16

--concurrency-range 1:8:2表示并发从 1 测到 8,步长 2;--input-data指向一个 JSON 文件,一行一条请求;--measurement-interval 10000是每个并发点测 10 秒,避免受 JIT 预热影响。输出里的Throughput是每秒完成的请求数,p90/p95 latency是长尾延迟。如果并发从 1 升到 4 吞吐几乎翻倍,再从 4 到 8 吞吐不涨但 p95 暴涨,说明 KV cache 已经接近上限,这时候不是加大并发,而是回去把max_num_tokens降一降,或缩短单条请求的最大输出长度。我在项目里会把这个基线存下来,作为后续调量化、调上下文和换卡后的对比依据。

5. 避坑:TensorRT-LLM 部署 Qwen1.5 的五个高频翻车现场

5.1 build 时报 Unsupported layer type,Qwen1.5 没被认出来

现象:执行trtllm-build时,console 里刷一串Unsupported layer type或KeyError: 'qwen2',engine 文件没有生成。

原因:TensorRT-LLM 版本太旧,model type 映射表里还没有 Qwen1.5/Qwen2 的条目;或者权重目录里config.json的architectures字段被改过,判断不了模型结构。这种情况在“clone 了最新源码但镜像 tag 还是半年前的”项目里非常常见。

解决:先看版本,git log或pip show tensorrt_llm确认版本。直接换成官方和源码配套的容器 tag,再去examples/qwen里确认脚本是否支持目标模型。不要自己改attention.py去适配,那个坑太深。换成新版本后重新走一遍 convert,大多能过。

5.2 build 过程显存溢出,被 OOM Kill

现象:编译到一半进程直接消失,dmesg里看到Out of memory,或者容器内出现Killed。

原因:TensorRT build 的峰值显存不只是模型权重,它会对所有候选 kernel 做 profiling,峰值可能比推理时高一大截。--max_num_tokens设得太大、--max_batch_size设得太大,都会让峰值超过显存上限。

解决:把--max_num_tokens降到 2048,--max_batch_size降到 4,--max_seq_len降到 4096 先验证流程;如果版本支持,打开--fast_build减少优化工作量。还有一个更土办法:build engine 时不要跑任何其他占显存的程序,包括不要挂着 Triton 容器。这个看着简单,但真的能省很多排错时间。

5.3 输出乱码、重复、中文变英文或变成拼音

现象:engine 加载成功,generate 出来的文本没有逻辑,有时是英文词语碎片,有时是同一个 token 反复刷屏。

原因:九成是 tokenizer 不匹配。engine 本身只有权重和超参,tokenizer 是独立的;加载 engine 时如果没有指定原始模型的 tokenizer 路径,TensorRT-LLM 可能用了默认或错误的 tokenizer,词表 id 对不上自然乱。另一个常见原因是权重和 tokenizer 来自不同模型,比如 Qwen1.5-7B 的权重配了 Qwen2-7B 的 tokenizer。

解决:加载时显式指定tokenizer_dir,在 Triton 的 config 里也写清楚tensorrt_llm_tokenizer_dir参数。转换时用同一个模型目录。如果多个模型文件混放,认准tokenizer.json的哈希是否和原模型一致。我把这招叫“tokenizer 是部署的黑匣子,出了问题先查它”。

5.4 并发一高,延迟发散,甚至 OOM

现象:perf_analyzer 显示并发 2 时 p95 只有 200ms,并发 4 时 p95 跑到 2s,吞吐还降了。

原因:KV cache 的预算被前面的长请求占满,后面的请求排队等显存。常见于max_num_tokens设置过大而max_seq_len也大,显存里能容纳的并发序列数少于max_batch_size,inflight batching 实际没产生并发。

解决:在 build 参数里限制总的 token 预算。我把max_num_tokens设置成「期望并发数 × 平均序列长度」:希望 8 并发、平均序列 512 token,就设 4096,不给富余;想保底就设 6144,并观察显存曲线。另外检查是否每个请求都申请了很大max_tokens,有些客户端框架默认把max_tokens设成很大,这会按最大长度预留 KV cache,自然容易 OOM。

5.5 服务起来后第一个请求特别慢,随后又正常

现象:Triton 加载模型后第一次调用,延迟 20~60 秒,触发超时告警,之后请求都正常。

原因:TensorRT 在第一次推理时需要初始化 cuBLAS/cuDNN 的 kernel 选择,会做一轮 benchmark 和 workspace 分配;部分版本还会在第一次 generate 时做内存对齐和 context 初始化。这不是 bug,是 TensorRT 的工作方式。

解决:上线前做 warmup。Triton 的模型实例加载后,用一个短请求提前触发初始化;也可以把 warmup 写进加载脚本,模型 ready 后立刻跑一次max_tokens=1的请求。我在生产里会在部署脚本中加一个warmup.py,请求文本固定为“你好”,等它返回后再把服务标记为健康,不然监控系统第一分钟全是红色告警。

6. 进阶:把上下文和并发调到能用的水平,以及上线前怎么验证

先算一笔 KV cache 的账。每个 token 的 KV 占用可以用公式估:2 × 层数 × KV head 数 × head_dim × 字节数。Qwen1.5-7B 是 28 层、4 个 KV head、head_dim 128,用 fp16 算是 2 × 28 × 4 × 128 × 2 ≈ 56KB 每 token,8K 上下文大约 0.45GB。所以 7B 在 24GB 卡上真正的瓶颈往往不是上下文,而是并发数乘上平均序列长度之后的累积。你设max_num_tokens = 4096,相当于每一轮 inflight batch 最多装这么多 token,太长或者太长尾的请求会把预算吃满。知道这个数之后,调并发就不是拍脑袋了。

如果显存实在不够,用 INT8/FP8 量化把权重砍半。TensorRT-LLM 的trtllm-build支持--quantize int8_weight_only或--quantize fp8,转换命令和普通 build 差别不大,但量化后必须做精度验证:对比同一个 prompt 在 fp16 和量化引擎上的输出,再看语义是否一致。A100 没有硬件 FP8,上 FP8 反而可能更慢;4090 和 H 系列用 FP8 收益明显。我个人只在显存不够时才量化,能跑 bf16 就优先 bf16。

上线前我习惯用这份清单过一遍:引擎目录是否备份;是否指定了正确的 tokenizer;用 perf_analyzer 测过 1/2/4/8 并发,p95 在容忍阈值内;连续跑 1000 次请求观察显存曲线,确认没有缓慢上涨;用至少 8K 长度的输入测过长上下文,确认没有在输出阶段 OOM。我吃过亏的地方是:第一版服务一直稳,结果某天同事传了一个 30K 的文档进来,直接 OOM 进程都没了。后来把所有模型的max_seq_len和服务层长度校验做死,才没再发生。

一句话教训:TensorRT-LLM 的 engine 是构建产物,要像二进制 artifact 一样管理;tokenizer 是容易被忽略的黑匣子,必须显式指定;性能要靠 perf_analyzer 的基线,不要靠感觉。这套流程虽然有点琐碎,但真能让你从“能跑”走到“敢上线”。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询