TensorRT-LLM部署Qwen1.5实战:从权重转换到推理性能优化
简介面向大语言模型部署实践者针对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 Capabilitynvidia-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 toolkitTensorRT-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-size2g --ulimit memlock-1 --ulimit stack67108864 \ -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_idQwen/Qwen1.5-7B-Chat, local_dir/data/models/Qwen1.5-7B-Chat, max_workers8 ) 注意 Qwen1.5 的仓库里有多个权重分片文件safetensorssnapshot_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-buildEngine 构建的关键参数权重转换只是预处理真正决定推理性能的是 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_dirstr(engine_dir), max_batch_size1, max_input_len2048, max_seq_len8192, free_gpu_memory_fraction0.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_tokens512, temperature0.7, top_p0.9, repetition_penalty1.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, tokenizeFalse, add_generation_promptTrue ) print(prompt) # 输出类似 # |im_start|system # 你是一个得力的AI助手。|im_end| # |im_start|user # 请用三句话解释什么是大语言模型。|im_end| # |im_start|assistant inputs tokenizer(prompt, return_tensorspt)[input_ids].tolist()[0] print(Prompt tokens:, len(inputs))add_generation_promptTrue 会在末尾追加|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_size1, max_input_len2048, max_seq_len8192, free_gpu_memory_fraction0.2, ) app.post(/v1/completions) async def generate(req: ChatRequest): outputs runner.generate( [req.prompt], max_new_tokensreq.max_tokens, temperaturereq.temperature, top_p0.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, tokenizeFalse, add_generation_promptTrue) # 生成逻辑同上服务化之后要面对并发问题。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-ng12覆盖系统库。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 是 4head_dim 是 128max_seq_len 设 32768 时光 KV cache 就要占2 * 28 * 4 * 128 * 8 * 32768 * 2也就是约 14.6G加上权重本身的 14G24G 卡必然爆。解决方法是先用nvidia-smi看引擎加载后剩余的显存再倒推 max_seq_len。或者更简单——直接降低 free_gpu_memory_fraction把 KV cache 预分配调小。但要注意这个值调太低会让长文本生成的第二个请求直接报 out of memory需要在并发能力和可用显存之间找平衡。5.3 生成的文本出现重复碎词或中文乱码现象是英文生成正常但中文输出里夹杂着大段的乱码或者同一个词组反复出现。原因大概率出在 tokenizer 和 engine 的 token 表不一致。Qwen1.5 用了 tiktoken 格式的 tokenizerTensorRT-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_windowself.context_window, num_outputself.num_output, model_nameself.model_name, is_chat_modelTrue, ) llm_chat_callback() def chat(self, messages, **kwargs: Any): prompt self._tokenizer.apply_chat_template( messages, tokenizeFalse, add_generation_promptTrue ) outputs self._runner.generate( [prompt], max_new_tokensself.num_output, temperature0.3, top_p0.85, ) text outputs[0].outputs[0].text return CompletionResponse(texttext) 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, tokenizeFalse, add_generation_promptTrue ) outputs runner.generate( [prompt], max_new_tokens256, temperature0.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 的其他尺寸版本或者迁移到新卡上基本都能在两小时内完成希望帮到你。本文还有配套的精品资源点击获取