计费系统对接:按Token数量统计TensorRT服务用量
在AI模型即服务(MaaS)的商业化浪潮中,一个看似简单却至关重要的问题浮出水面:用户用一次大模型API,到底该收多少钱?
如果只是按调用次数收费,那发送“你好”和生成一篇3000字报告的成本显然不该一样。GPU的显存、计算时间、功耗都与处理的数据量直接相关——而这个“数据量”,在语言模型的世界里,最合理的度量单位就是Token。
于是,如何精准捕捉每一次推理过程中输入输出的Token数量,并将其无缝接入计费系统,成为构建可持续AI服务平台的关键一环。这其中,NVIDIA TensorRT 凭借其对动态序列长度的支持和极致性能优化能力,成为了实现这一目标的理想载体。
为什么是TensorRT?
要理解为何选择TensorRT作为计量基础,得先看清它的本质:它不是一个训练框架,也不是一个通用推理库,而是一个为部署而生的编译器。
当你把一个PyTorch或TensorFlow模型导出为ONNX格式后,TensorRT会对其进行深度“手术式”优化:
- 把多个小算子融合成一个高效内核(比如 Conv + ReLU → fused_conv_relu),减少GPU调度开销;
- 将FP32权重压缩到FP16甚至INT8,在几乎不损失精度的前提下提升吞吐;
- 针对特定GPU架构(如A100、T4)自动挑选最快的CUDA kernel组合;
- 支持动态输入形状,允许同一引擎处理从1个词到上千个Token的不同请求。
这些特性意味着什么?
意味着你可以在高并发场景下,依然保证每个请求都能以毫秒级延迟完成推理,同时还能准确知道这次推理究竟“吃了多少资源”。
更重要的是,由于TensorRT在构建阶段就明确了输入输出张量的结构,运行时可以轻松获取shape信息——这正是提取Token数量的技术前提。
例如,在LLM服务中,输入通常是[batch_size, seq_len]形状的input_ids。只要在推理前后读取seq_len,就能得到输入Token数;同理,解码完成后返回的output_ids也携带了生成长度。两者相加,便是本次调用的实际消耗。
import tensorrt as trt import numpy as np TRT_LOGGER = trt.Logger(trt.Logger.WARNING) def build_engine_onnx(model_path: str): with trt.Builder(TRT_LOGGER) as builder, \ builder.create_network(flags=1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) as network, \ trt.OnnxParser(network, TRT_LOGGER) as parser: config = builder.create_builder_config() config.max_workspace_size = 1 << 30 # 1GB config.set_flag(trt.BuilderFlag.FP16) with open(model_path, 'rb') as f: if not parser.parse(f.read()): print("ERROR: Failed to parse .onnx file") return None # 动态序列支持:关键! profile = builder.create_optimization_profile() profile.set_shape('input_ids', (1, 1), (1, 64), (1, 128)) config.add_optimization_profile(profile) return builder.build_engine(network, config)上面这段代码中的set_shape调用,正是让TensorRT能够处理变长输入的核心。没有这一步,所有请求就必须 padding 到固定长度,造成资源浪费,也无法实现细粒度计量。
如何把Token变成账单?
设想这样一个典型的服务链路:
[客户端] ↓ [API网关] → [鉴权/限流] ↓ [调度器] → [负载均衡] ↓ [TensorRT实例] ← 提取 input_len / output_len ↓ [异步上报] → Kafka / Redis ↓ [聚合服务] → 按天汇总用量 ↓ [账单系统] → 发票生成整个流程中最关键的设计在于:不能因为计费影响推理性能。
我们曾见过不少团队在主线程中直接写数据库记录用量,结果导致P99延迟飙升。正确的做法是——快进快出,异步脱钩。
具体来说:
- 用户请求到达后,立即使用Tokenizer将文本转为
input_ids,此时即可获得input_len = input_ids.shape[1]; - 将
input_ids送入TensorRT引擎执行推理; - 得到
output_ids后,立刻计算output_len = output_ids.shape[1]; - 构造一条轻量日志:
json { "user_id": "u_12345", "req_id": "r_67890", "model": "llama-2-7b-trt", "input_tokens": 85, "output_tokens": 142, "total_tokens": 227, "timestamp": 1712345678.123 } - 通过Kafka或Redis等消息中间件异步推送,主路径不等待响应;
- 后台消费者服务批量拉取数据,按小时/天聚合,写入计费数据库。
这种设计不仅避免了I/O阻塞,还带来了额外好处:具备重放和审计能力。万一出现争议,你可以回溯每一条原始记录,而不是依赖汇总后的数字。
实际挑战与应对策略
1. 不同模型Tokenizer不统一怎么办?
GPT系列用Byte-Level BPE,Llama用SentencePiece,ChatGLM又是自己的分词逻辑……直接暴露底层差异会给计费系统带来混乱。
解决方案是在服务层做抽象封装:
class TokenCounter: _counters = {} @classmethod def register(cls, model_name, func): cls._counters[model_name] = func def count(self, model_name, text): if model_name not in self._counters: raise ValueError(f"Unsupported model: {model_name}") return self._counters[model_name](text) # 注册不同模型的计数逻辑 TokenCounter.register("gpt-3.5-turbo", lambda t: len(gpt_tokenizer.encode(t))) TokenCounter.register("llama-2", lambda t: len(llama_tokenizer.tokenize(t)))对外只暴露count_tokens(model, text)接口,计费系统无需关心细节。
2. 推理失败了还要记账吗?
当然要,但得合理。
比如用户提交了一个极长的prompt,触发了上下文溢出(context overflow),模型未能生成任何输出。这时候虽然没产出内容,但GPU已经完成了KV缓存构建、注意力计算等一系列操作,资源已被占用。
建议规则:
- 成功响应:计入
input_tokens + output_tokens - 失败但有部分输出(如early stopping):计入实际生成的output长度
- 完全无输出且因客户端错误(如超长输入):至少计入input_tokens的50%,体现资源预占成本
- 服务端崩溃:不计费,需标记异常事件供运维排查
这样既能防止恶意刷量,也能保护用户体验。
3. 如何防止数据被篡改或伪造?
Token统计数据一旦进入计费流程,就必须具备防篡改能力。尤其是在多租户环境下,用户可能尝试伪造trace_id来规避费用。
推荐措施:
- 所有上报数据附带服务端签名(HMAC-SHA256),密钥仅存在于可信节点;
- 使用唯一请求ID(UUIDv4)而非自增ID,防止猜测;
- 在API网关层注入不可变字段(如client_ip、user_agent哈希);
- 关键字段(如token数)在推理完成后由服务端独立计算,不信任客户端声明值。
4. 性能监控怎么做?
除了计费,Token维度的数据本身也是极佳的观测指标。
建议在Prometheus中暴露以下metrics:
# HELP trt_inference_tokens_total Total number of tokens processed # TYPE trt_inference_tokens_total counter trt_inference_tokens_total{model="llama-2-7b", type="input"} 1234567 trt_inference_tokens_total{model="llama-2-7b", type="output"} 890123 # HELP trt_tokens_per_second Current throughput in tokens/sec # TYPE trt_tokens_per_second gauge trt_tokens_per_second{gpu="A100-40GB"} 1420.5配合Grafana面板,你可以实时看到:
- 当前集群每秒处理多少Token;
- 哪些模型负载最高;
- 平均每次请求消耗多少Token;
- 是否存在异常突增(可能是爬虫或攻击)。
这些数据不仅能用于容量规划,还能反向指导定价策略。
工程实践中的几个“坑”
别看流程清晰,真正在生产环境落地时,有几个细节特别容易踩雷:
✅ 动态Shape范围必须合理设置
如果你把max_seq_len设成1024,但某次请求来了个2048长度的输入,TensorRT会直接报错。更糟的是,有些版本的引擎会在首次遇到超限输入时重建引擎,导致严重延迟毛刺。
建议:
- 根据业务场景设定上限(如客服对话一般不超过512,文档摘要可放宽至2048);
- 超限时提前截断并告警,不要交给引擎处理;
- 对于超大文本需求,提供异步批处理通道。
✅ INT8量化需谨慎启用
虽然INT8能让吞吐翻倍,但它需要校准过程,且对某些模型(尤其是小参数量或微调过的)可能导致精度显著下降。
建议:
- 新模型上线优先使用FP16;
- 对稳定模型进行AB测试对比INT8与FP16的输出质量;
- 只在非敏感场景(如推荐、搜索)开启INT8;
- 计费系统中标记所用精度模式,便于后续分析。
✅ 构建阶段耗时不容忽视
一个复杂模型的TensorRT引擎构建可能需要几分钟甚至几十分钟。如果你每次部署都重新构建,CI/CD流程会被拖垮。
解决方案:
- 引擎构建与部署分离:构建一次,多处部署;
- 使用缓存机制(如基于模型hash+GPU型号生成唯一key);
- 在专用构建机上运行,避免占用推理资源。
结语
当AI服务从“能用”走向“好用”,再到“可用作生意”时,精细化资源计量就成了绕不开的一环。
以Token为单位,依托TensorRT提供的动态输入支持和高性能执行环境,我们可以做到既不影响推理效率,又能精确捕捉每一次调用的真实资源消耗。
这种“用多少付多少”的模式,不只是技术实现,更是一种信任机制的建立:让用户清楚每一笔费用的来源,也让平台能公平回收成本。
未来,随着MaaS生态的成熟,这类底层计量能力将成为基础设施的一部分——就像水电表之于公用事业。而今天在TensorRT中埋下的每一个shape[1]读取操作,都是在为那个可度量、可审计、可持续的AI时代打地基。