ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TensorRT-LLM部署Qwen1.5实战:从引擎编译到Triton服务化

TensorRT-LLM部署Qwen1.5实战:从引擎编译到Triton服务化 简介面向大模型部署与推理优化工程师提供基于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 放不下 bf16Qwen1.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-size2g --ulimit memlock-1 \ -v ${PWD}:/workspace \ nvcr.io/nvidia/tritonserver:24.05-trtllm-python-py3两个容易忽略的参数--shm-size2g是因为 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为什么是 bfloat16TensorRT-LLM 不能直接吃 Hugging Face 的 safetensors它需要先把权重转成自己的 checkpoint 格式再编译成 TensorRT engine。转换脚本在仓库的examples/qwen目录下# 转成 TensorRT-LLM checkpointtp_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 后面写 1engine 加载会直接报权重 shape 不匹配。--dtype我强烈建议用bfloat16除非你是 V100 这种不支持 bf16 的老卡bf16 的指数位更宽在长序列场景下不容易溢出FP16 虽然占一样显存但偶尔会把 loss 跑飞。如果转换脚本找不到 Qwen1.5 的模型定义先确认 TensorRT-LLM 版本是否包含qwen示例以及config.json里的model_type。Qwen1.5 的架构走的是 Qwen2 的代码路径model_type通常是qwen2TensorRT-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_tokens256, temperature0.7, top_p0.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-size4g \ -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 typeQwen1.5 没被认出来现象执行trtllm-build时console 里刷一串Unsupported layer type或KeyError: qwen2engine 文件没有生成。原因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_sizeinflight 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_tokens1的请求。我在生产里会在部署脚本中加一个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 每 token8K 上下文大约 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 的基线不要靠感觉。这套流程虽然有点琐碎但真能让你从“能跑”走到“敢上线”。希望帮到你。本文还有配套的精品资源点击获取
返回列表