简介:面向大语言模型部署实践者,针对TensorRT-LLM优化部署Qwen1.5模型,内容覆盖从环境配置、模型转换到推理引擎部署的全流程演示。项目提供完整Python源码与配套Markdown教程,实际解决推理速度慢、硬件资源占用高等部署难题,适合具备一定深度学习基础、希望工程化落地大模型的开发者参考学习。压缩包共含5个文件,包括4个.py脚本与1个说明文档,脚本覆盖模型参数处理、层工具封装、核心推理逻辑与检查点转换等功能,README则逐步讲解环境搭建和性能测试方法,整体仅25KB,便于快速下载阅读源代码并迁移至自有项目。当前已有592人学习下载,内容聚焦TensorRT-LLM与Qwen1.5的深度适配,通过引入层融合、内核自动调优等优化手段,直观展示高吞吐低延迟的部署方案。读者既能获得可直接复用或改造的工程代码,也能借助教程理清大模型部署的关键思路,是动手实践大模型推理优化的一份优质参考。
1. 把 Qwen1.5 跑成生产级推理服务:TensorRT-LLM 才是那个值得折腾的部署方案
最近在帮客户落地 Qwen1.5 的私有化部署,前后对比了 vLLM、llama.cpp 和 HuggingFace 原生管线,最终把主线方案定在 TensorRT-LLM 上。原因很简单:同样的 A100 80G,原生 PyTorch 推理 Qwen1.5-14B 只能吃到 1200 tokens/s 左右的生成速度,换 TensorRT-LLM 做完图优化后直接翻了一倍多,显存占用还降了 30% 以上。这个差距对单机部署是质变级的,尤其面对高并发请求或长上下文场景,慢 200ms 就是完全不同的用户体验。这篇笔记从环境准备、权重转换、engine 构建、运行时调优四个层面,把整套流程和踩过的坑都拆开讲,适合正在做千问大模型本地部署、或者想从 vLLM 迁移到 TensorRT-LLM 的工程师。
2. 环境准备:TensorRT-LLM 为什么对硬件和驱动这么挑
TensorRT-LLM 的本质是把大模型的算子层、显存布局和调度逻辑全部编译成针对特定 GPU 架构的 CUDA 内核,这意味着它对环境的要求远比普通 PyTorch 项目苛刻。你没法指望在任何一台机器上 pip install 就能跑起来,版本匹配问题能直接耗掉你半天时间。
2.1 GPU 选型与算力下限
TensorRT-LLM 的每一版 release 都明确列出了支持的 GPU 算力代号。Qwen1.5 系列模型参数从 0.5B 到 72B 都有,但部署目标至少需要 Ampere 架构以上的显卡,也就是算力 8.0 起的 A100/A30/A10 以及消费级的 RTX 30 系、40 系。如果你手头只有 GTX 1080 Ti 这类 Pascal 架构,直接放弃,TensorRT-LLM 官方压根不编译对应内核。
我一般会用 nvidia-smi 先确认驱动支持的最高 CUDA 版本,再看 GPU 的 Compute Capability:
nvidia-smi # 输出里看 "Driver Version" 和 "CUDA Version" 两列 # 例如 Driver Version: 535.104.05, CUDA Version: 12.2 python -c "import torch; print(torch.cuda.get_device_capability())" # 输出类似 (8, 0) 代表 Ampere A100, (8, 9) 是 RTX 4090CUDA 版本要看的是右侧的 "CUDA Version" 而不是驱动版本,它表示驱动能支持的最高运行时版本。TensorRT-LLM 0.9 及之后版本依赖 CUDA 12.x,如果你机器的驱动只到 CUDA 11.8,就得降级老版本 TensorRT-LLM,这个兼容矩阵在后面踩坑章节再展开。
提示:生产环境不要直接用 conda 装 CUDA toolkit,TensorRT-LLM 编译和运行时找的是系统级 CUDA 路径,用 docker 镜像是最省心的方式。
2.2 三套环境方案的取舍
部署环境我试过三条路,各有各的适用场景。
第一条是 NVIDIA 官方 NGC 容器。镜像里已经预装了匹配好的 TensorRT、CUDA、cuDNN 和 TensorRT-LLM,不做二次开发的话直接拉取最省事:
docker pull nvcr.io/nvidia/tritonserver:24.01-trtllm-python-py3 docker run -it --gpus all --shm-size=2g --ulimit memlock=-1 --ulimit stack=67108864 \ -v /data/models:/models nvcr.io/nvidia/tritonserver:24.01-trtllm-python-py3 bash容器内自带完整的 TensorRT-LLM,包括 weights 转换脚本和 trtllm-build 工具。缺点是镜像体积大,而且 TensorRT-LLM 版本是固定的,后续要升级只能换 tag。
第二条是源码编译,适合要魔改模型结构或调试底层算子的场景。TensorRT-LLM 源码编译需要 16G 以上内存和大约 30 分钟到 1 小时:
git clone https://github.com/NVIDIA/TensorRT-LLM.git cd TensorRT-LLM git submodule update --init --recursive make -C docker build第三条也就是我最终采用的方案——在干净的基础 PyTorch 镜像上手动安装预编译 wheel。TensorRT-LLM 的 release 页面会提供对应 CUDA 版本的 wheel 包,装上之后再把 tensorrt 和 cudnn 用 pip 版本对齐,灵活性和稳定性相对平衡。
2.3 网络模型下载的边界处理
Qwen1.5 权重需要从 HuggingFace 拉取,但生产机器经常访问不了外网。常见做法是在有网的机器上下载对应模型的 snapshot,用 huggingface_hub 的 snapshot_download 把整个仓库(包括 tokenizer 配置和模型权重)完整拉下来,再压缩传到内网。
pip install huggingface_hub python -c " from huggingface_hub import snapshot_download snapshot_download( repo_id='Qwen/Qwen1.5-7B-Chat', local_dir='/data/models/Qwen1.5-7B-Chat', max_workers=8 ) "注意 Qwen1.5 的仓库里有多个权重分片文件(safetensors),snapshot_download 的 max_workers 可以加速并行下载。传到内网后需要核对文件完整性,重点看 safetensors 的最终修改时间和文件大小,少了任何一个分片后面转换权重时都会报出莫名其妙的维度错误。
3. 核心流程:把 Qwen1.5 的 HuggingFace 权重转成 TensorRT-LLM Engine
这才是整套流程里最让人头大的部分,涉及权重格式转换、模型定义编写和 engine 构建三步。Qwen1.5 的模型结构跟 LLaMA 有一定差异,好在 TensorRT-LLM examples 目录下已经有官方支持的 Qwen 示例脚本,不需要自己写模型定义。
3.1 权重转换:从 PyTorch 到 TensorRT-LLM 的 Checkpoint 格式
TensorRT-LLM 不能直接加载 HuggingFace 的 safetensors 权重,需要先用 convert_checkpoint.py 脚本将权重转成 TensorRT-LLM 自己的 checkpoint 存储格式。这一步的本质是把 HuggingFace 的模型字典重新排列成 TensorRT-LLM 的权重布局,同时把 dtype 处理成后续 graph 优化需要的格式。
cd TensorRT-LLM/examples/qwen python convert_checkpoint.py \ --model_dir /data/models/Qwen1.5-7B-Chat \ --output_dir /data/trt_ckpt/Qwen1.5-7B-Chat-fp16 \ --dtype float16 \ --tp_size 1 \ --pp_size 1参数含义:model_dir 指向 HuggingFace 原始权重目录;output_dir 是转换后 checkpoint 的输出路径;tp_size 是张量并行度,单卡就设 1,多卡按 GPU 数设置;pp_size 是流水线并行度,一般单机场景用不上,保持 1。dtype 选 float16 是为了后续能配合 FP16 的 gemm 插件做混合精度推理,如果显存富余也可以选 bfloat16,精度表现更好。
转换完成后检查输出目录,看到 config.json、model.layernorm.weight.bin、model.layers.X.*.bin 这一组文件才算成功。tp_size 大于 1 时,每层权重会被切成多份,文件名会带上tp_rank后缀,这是正常的。
3.2 trtllm-build:Engine 构建的关键参数
权重转换只是预处理,真正决定推理性能的是 trtllm-build 阶段。这一步会把 TensorRT-LLM checkpoint 编译成针对当前 GPU 架构优化的 engine 文件,同时决定 KV cache 大小、算子融合策略、量化格式等关键指标。
trtllm-build \ --checkpoint_dir /data/trt_ckpt/Qwen1.5-7B-Chat-fp16 \ --gemm_plugin float16 \ --gpt_attention_plugin float16 \ --max_batch_size 8 \ --max_input_len 2048 \ --max_seq_len 8192 \ --output_dir /data/trt_engine/Qwen1.5-7B-Chat-fp16 \ --workers 4参数说明逐一说。gemm_plugin 是矩阵乘法的插件开关,float16 表示矩阵乘法用半精度跑,这是性能提升的最主要来源;gpt_attention_plugin 对应 attention 部分的优化,必须打开,否则 Qwen1.5 的 GQA 注意力机制退化到普通多头实现,速度会掉一半。max_batch_size 决定并发能力,但它直接乘以 max_seq_len 后影响 KV cache 的显存预分配,8 和 8192 的组合在 24G 显存卡上是比较保守的,后面细说怎么调。max_input_len 限制用户输入的 token 数,max_seq_len 是输入加输出的总长。workers 是并行编译的线程数,机器核多就调大,能明显缩短编译时间。
构建完成后 output_dir 下会生成一个qwen_7b_chat_fp16_tp1_pp1_bs8_seq8192之类的目录,里面的rank0.engine才是运行时需要的文件。
3.3 多卡并行与量化格式怎么选
Qwen1.5-14B 以上的模型单卡放不下,需要 tp_size 配合多卡。tp_size 改成 2 之后,convert_checkpoint 阶段需要指定--tp_size 2,且 trtllm-build 后输出的 engine 目录里会同时出现 rank0.engine 和 rank1.engine 两个文件。
# 双卡场景的完整流程 python convert_checkpoint.py \ --model_dir /data/models/Qwen1.5-14B-Chat \ --output_dir /data/trt_ckpt/Qwen1.5-14B-Chat-fp16-tp2 \ --dtype float16 --tp_size 2 --pp_size 1 trtllm-build \ --checkpoint_dir /data/trt_ckpt/Qwen1.5-14B-Chat-fp16-tp2 \ --gemm_plugin float16 --gpt_attention_plugin float16 \ --max_batch_size 4 --max_input_len 2048 --max_seq_len 4096 \ --output_dir /data/trt_engine/Qwen1.5-14B-Chat-fp16-tp2显存紧张还想硬上更大模型的话,可以在 convert_checkpoint 时加上--use_weight_only --weight_only_precision int8或 int4,这对应 TensorRT-LLM 的权重复用(weight-only)量化,只量化权重不量化激活。INT8 权重量化后 14B 模型显存占用能降到 9G 左右,但解码速度会有不到 10% 的损耗,属于保显存换速度的折中。
4. 用 Python Runtime 加载 Engine 跑通一次完整推理
engine 文件是编译后的产物,没法直接用 PyTorch 的 from_pretrained 加载。TensorRT-LLM 提供了两种运行方式:Python bindings 适合快速验证和二次开发,C++ runtime 适合极致性能但对代码能力要求高。这里只讲 Python 方案够用的那套。
4.1 最小推理代码与流式输出
import time from pathlib import Path from tensorrt_llm.runtime import ModelRunnerCpp engine_dir = Path("/data/trt_engine/Qwen1.5-7B-Chat-fp16") runner = ModelRunnerCpp( engine_dir=str(engine_dir), max_batch_size=1, max_input_len=2048, max_seq_len=8192, free_gpu_memory_fraction=0.2, ) messages = [ {"role": "system", "message": "你是一个得力的AI助手。"}, {"role": "user", "message": "请用三句话解释什么是大语言模型。"} ] prompt = build_qwen_chat_prompt(messages) # 按 Qwen1.5 对话模板拼接 t0 = time.time() outputs = runner.generate( [prompt], max_new_tokens=512, temperature=0.7, top_p=0.9, repetition_penalty=1.1, ) print("Total time:", time.time() - t0) print("Generated:", outputs[0].outputs[0].text)代码中的 build_qwen_chat_prompt 需要按 Qwen1.5 的 chat template 拼装完整消息。Qwen1.5 用的是<|im_start|>格式:系统消息、用户消息和助手回复之间用特殊 token 分隔。可以直接用 transformers 的 AutoTokenizer 的 apply_chat_template 方法来生成,避免手拼出错。
ModelRunnerCpp 的几个初始化参数里,engine_dir 指向包含 rank*.engine 的目录;max_batch_size、max_input_len、max_seq_len 必须和 trtllm-build 时的参数保持一致或更小,不能超过构建时设置的上限,否则运行期会报错。free_gpu_memory_fraction 是给 KV cache 留的显存比例,0.2 表示把引擎占满之后的剩余显存再留 20% 做缓冲。
4.2 对话输入拼接的正确姿势
拼接 prompt 是这里最容易翻车的地方。Qwen1.5 的 tokenizer 要求特殊 token 必须以连续形式出现,任何多余的空格、换行都会改变 token 切分结果。最可靠的方式是用 transformers 库来拼接。
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained("/data/models/Qwen1.5-7B-Chat") prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) print(prompt) # 输出类似: # <|im_start|>system # 你是一个得力的AI助手。<|im_end|> # <|im_start|>user # 请用三句话解释什么是大语言模型。<|im_end|> # <|im_start|>assistant inputs = tokenizer(prompt, return_tensors="pt")["input_ids"].tolist()[0] print("Prompt tokens:", len(inputs))add_generation_prompt=True 会在末尾追加<|im_start|>assistant\n>,让模型知道该生成回复了。生成完的文本里如果带着<|im_end|>token,后处理的时候用text.split("<|im_end|>")[0]截掉即可。
4.3 服务化部署:用 FastAPI 把 Engine 包成 OpenAI 兼容接口
单机脚本验证没问题后,就要考虑服务化。TensorRT-LLM 生态里最省事的做法是用 FastAPI 自包一个服务端,复用上面的 runner,把输入请求转发给 engine 并同步流式返回。
from fastapi import FastAPI from fastapi.responses import StreamingResponse from pydantic import BaseModel app = FastAPI() runner = None class ChatRequest(BaseModel): prompt: str max_tokens: int = 512 temperature: float = 0.7 @app.on_event("startup") def load_engine(): global runner runner = ModelRunnerCpp( engine_dir="/data/trt_engine/Qwen1.5-7B-Chat-fp16", max_batch_size=1, max_input_len=2048, max_seq_len=8192, free_gpu_memory_fraction=0.2, ) @app.post("/v1/completions") async def generate(req: ChatRequest): outputs = runner.generate( [req.prompt], max_new_tokens=req.max_tokens, temperature=req.temperature, top_p=0.9, ) return {"text": outputs[0].outputs[0].text} @app.post("/v1/chat/completions") async def chat(req: ChatRequest): messages = [{"role": "user", "content": req.prompt}] prompt = tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) # 生成逻辑同上服务化之后要面对并发问题。ModelRunnerCpp 是线程安全的,可以放在多个线程里同时调用,但 max_batch_size 设的是 1 时并发请求会排队,性能提升有限。想要真正的高并发,需要在 trtllm-build 阶段把 max_batch_size 调大,然后运行时把多个请求合并成一个 batch 传给 runner,这是下一节要展开的调优方向。
5. 部署避坑指南:Qwen1.5 在 TensorRT-LLM 上最常见的五个问题
写这部分之前,我先回忆了一下自己这几周踩过的、以及帮别人排查过的所有报错。TensorRT-LLM 的报错信息经常是误导性的,一个 CUDA OOM 背后可能藏着 drei 种完全不同的原因。下面这五个问题是出现频次最高的,每一条都按现象、原因、解决来写。
5.1 编译时报错 undefined symbol 或 GLIBCXX not found
现象是 trtllm-build 阶段跑到一半直接崩溃,报一堆 cublas 或 cudnn 的 undefined symbol,或者 GLIBCXX_3.4.30 not found。
原因是 TensorRT-LLM 的 wheel 包依赖特定版本的 cuBLAS/cuDNN,而你的系统环境里存在多个 CUDA 版本,动态链接时 LD_LIBRARY_PATH 指向了错误的那套库。GLIBCXX 报错则是系统 libstdc++ 版本太老,conda 的 gcc 库和系统 gcc 库发生了冲突。
解决方法是首先确认当前环境的 CUDA 路径指向正确。用echo $LD_LIBRARY_PATH看输出,把不需要的 CUDA 路径全部清掉,然后通过 pip 安装 TensorRT-LLM 要求的 tensorrt 和 cudnn 配套版本。如果还是报 GLIBCXX,直接在 conda 环境里conda install -c conda-forge libstdcxx-ng=12覆盖系统库。
5.2 Engine 构建成功但推理时 OOM
现象是 trtllm-build 很顺利,engine 文件也生成了,但一跑推理就 CUDA OOM,或者是偶发的 allocation failed。
原因往往是 max_seq_len 设置得过大。KV cache 的显存占用近似等于2 * num_layers * num_kv_heads * head_dim * max_batch_size * max_seq_len * dtype_size。以 Qwen1.5-7B 为例,28 层、GQA 的 kv_heads 是 4,head_dim 是 128,max_seq_len 设 32768 时,光 KV cache 就要占2 * 28 * 4 * 128 * 8 * 32768 * 2,也就是约 14.6G,加上权重本身的 14G,24G 卡必然爆。
解决方法是先用nvidia-smi看引擎加载后剩余的显存,再倒推 max_seq_len。或者更简单——直接降低 free_gpu_memory_fraction,把 KV cache 预分配调小。但要注意,这个值调太低会让长文本生成的第二个请求直接报 out of memory,需要在并发能力和可用显存之间找平衡。
5.3 生成的文本出现重复碎词或中文乱码
现象是英文生成正常,但中文输出里夹杂着大段的�乱码,或者同一个词组反复出现。
原因大概率出在 tokenizer 和 engine 的 token 表不一致。Qwen1.5 用了 tiktoken 格式的 tokenizer,TensorRT-LLM 的 Qwen 示例里专门适配过,但如果你用老版本的 convert_checkpoint.py,可能会导致 tokenizer.json 和 model.vocab 没有同步过去。
解决方法是在 convert_checkpoint 之后检查输出目录里的 tokenizer.json 文件大小是否和 HuggingFace 原始的一致。如果不一致,手动从原始模型目录拷贝一份 tokenizer.json 和 tokenizer_config.json 到 engine 目录旁,运行时显式指定 tokenizer_dir 参数。
5.4 并发请求吞吐量完全没提升
现象是多个请求同时进来,响应时间线性增长,QPS 上不去,GPU 利用率跑不满。
原因是 ModelRunnerCpp 的 max_batch_size 虽然在构建时设了 8,但运行时逐条调用 generate 时每个请求独立进出,没有做 batching,等同于每次只推理一条。TensorRT-LLM 的 in-flight batching 需要在服务层自己实现请求排队和 batch 组装。
解决方法是参考 TensorRT-LLM 里 examples 自带的 simple_server 实现,把多个请求收集到一个队列,攒到 max_batch_size 或一个 timeout 周期后再统一调用 runner.generate。这个改造不复杂,但吞吐量能从 2-3 QPS 直接拉到 20+ QPS。
5.5 微调过的模型转换后输出完全不对
现象是拿 Qwen1.5 做 LoRA 或全参微调后,用同样的流程转换权重,推理结果和原版模型差异巨大,甚至输出乱码。
原因是微调后的模型权重里可能带了新增的 embedding 或 lm_head 维度变动,而 convert_checkpoint.py 默认按原始 Qwen1.5 结构读取权重文件。额外的 LoRA adapter 权重如果没有 merge 回主权重,TensorRT-LLM 转换时就会忽略这部分参数,导致输出偏移。
解决方法是转换前用python -m peft先把 LoRA adapter merge 回基础模型,保存成新的完整 HuggingFace 仓库,再走正常转换流程。微调时如果改了 tokenizer,还要确认词表大小一致,不一致的话得重建 tokenizer 并重新转换。
6. 进阶技巧:用 LlamaIndex 接入 Engine 跑 Retrieval-Augmented Generation
引擎部署稳定之后,下一个自然的需求是让模型能基于私有文档回答问题。TensorRT-LLM 的 engine 本身不提供 embedding 能力,但可以把它包装成 LlamaIndex 的 custom LLM 类,用现成的 embedding 模型配合做 RAG 应用。
6.1 包装 TensorRT-LLM Engine 为 LlamaIndex LLM 接口
from typing import Any, Optional from llama_index.core.llms import CustomLLM from llama_index.core.llms.callbacks import llm_chat_callback from llama_index.core.base.llms.types import CompletionResponse, LLMMetadata class TRTLLM(CustomLLM): context_window: int = 8192 num_output: int = 512 model_name: str = "Qwen1.5-7B-Chat-TRT" def __init__(self, runner, tokenizer): super().__init__() self._runner = runner self._tokenizer = tokenizer @property def metadata(self) -> LLMMetadata: return LLMMetadata( context_window=self.context_window, num_output=self.num_output, model_name=self.model_name, is_chat_model=True, ) @llm_chat_callback() def chat(self, messages, **kwargs: Any): prompt = self._tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True ) outputs = self._runner.generate( [prompt], max_new_tokens=self.num_output, temperature=0.3, top_p=0.85, ) text = outputs[0].outputs[0].text return CompletionResponse(text=text) def complete(self, prompt: str, **kwargs: Any): return self.chat([{"role": "user", "content": prompt}])把 CustomLLM 子类实例化后塞给 LlamaIndex 的 VectorStoreIndex.from_documents,就能让 Qwen1.5 在本地知识库上做基于检索的生成。注意 temperature 要调低到 0.2-0.4,因为 RAG 场景要求模型严格贴合检索到的上下文,过高的温度会让模型自由发挥,答非所问。
6.2 Kv Cache 复用与多轮会话的显存管理
多轮对话场景下,每次请求都重新计算历史 prompt 的 KV cache 是巨大的浪费。TensorRT-LLM 支持 KV cache 复用,前提是服务层保存了会话上下文。
# 多轮对话中保留历史 KV cache 的典型写法 conversation_history = [] def chat_with_history(user_input: str) -> str: conversation_history.append({"role": "user", "content": user_input}) prompt = tokenizer.apply_chat_template( conversation_history, tokenize=False, add_generation_prompt=True ) outputs = runner.generate( [prompt], max_new_tokens=256, temperature=0.7, ) reply = outputs[0].outputs[0].text conversation_history.append({"role": "assistant", "content": reply}) return reply这里没有做显式的 KV cache 保存,ModelRunnerCpp 内部对同样前缀的 prompt 会复用部分 cache,但因为每次都完整传入历史,实际几乎不会触发真正的前缀复用。想要真正省显存,需要手动把每次生成的 KV cache 存下来传回下一个请求,这属于 TensorRT-LLM 的会话级优化,代码复杂度高、收益大概在 20%-30% 的显存节省,适合长会话场景。
6.3 性能验证的最终检查清单
部署完毕别急着收工,照着下面这套检查单跑一遍,能确认你的部署是否真的达到了预期性能。
用nvidia-smi dmon -s u -d 1观察 GPU 利用率,正常推理时 SM 利用率应该在 80% 以上,如果是 30% 以下就该查是不是 batching 没做对。首 token 延迟用curl -w看有网络开销的真实时延,Qwen1.5-7B 在 A100 上首 token 应该在 50ms 以内。然后连续发 100 个请求测吞吐和显存增长,确认没有显存泄漏。最后检查不同长度 prompt 的耗时曲线,如果不随长度线性增长,说明 attention 的计算效率有问题,多半是 gpt_attention_plugin 没有生效。
这套流程我前后调了很多轮,最深的体会是 TensorRT-LLM 的报错和性能瓶颈很少能一眼看穿,多数时候要结合 nvidia-smi、日志和分段计时综合判断。把上面这些经验记下来之后,再部署 Qwen1.5 的其他尺寸版本或者迁移到新卡上,基本都能在两小时内完成,希望帮到你。
本文还有配套的精品资源,点击获取