
摘要
推理服务上线即OOM、首token延迟破秒、吞吐不及压测一半。本文从某智能问答平台真实复盘切入,剖析KV cache膨胀、动态批fill/prefill冲突、量化长尾翻车、冷启动失效、多租户抢算力五个痛点,给出PagedAttention、PD分离、敏感度量化、零拷贝、令牌桶的量化方案。
1. KV cache才是显存真正的吞噬者
痛点现场
某智能问答平台用Llama-70B部署推理服务,压测单卡A100能跑20并发,上线后并发到10就OOM崩溃。团队以为是模型权重太大(70B FP16约140GB),切到两卡TP部署后单卡权重降到70GB,理论上80GB显存还有10GB余量,但并发到15又OOM。
排查发现真正的元凶是KV cache。70B模型有80层,每层attention的KV cache随batch和序列长度线性增长------单请求2048 token的KV cache约2.5GB,15并发就是37.5GB,加上权重70GB直接撑爆80GB显存。压测时用的短文本(512 token)KV cache只有0.6GB/请求,掩盖了真实负载。
更隐蔽的是KV cache显存碎片化。朴素分配是按请求整块申请显存,请求结束后释放,反复申请释放造成显存空洞化,新请求找不到连续空间即使总余量够也OOM。某项目连续运行3小时后显存利用率98%但实际可用只有40%,全是碎片。
根因剖析
KV cache膨胀的机理是attention的自回归特性。每生成一个token,模型要访问前面所有token的K和V,因此需把历史KV缓存下来。KV cache大小 = 2(K和V)× num_layers × hidden_dim × seq_len × batch × dtype_size,对所有线性增长于seq_len和batch。
碎片化的根因是朴素显存分配按请求整块管理,请求长度不一导致块大小不一,释放后形成大小不等的空洞。PyTorch的CUDA caching allocator虽做了池化,但对KV cache这种大块高频分配释放场景仍易碎片。
巨程方案:PagedAttention分页KV cache
PagedAttention的机理是把KV cache按固定大小(block_size=16)分页,逻辑序列的块通过页表映射到物理块池的任意位置。物理块按需从空闲池取,请求结束归还,彻底消除碎片------物理块大小统一,无空洞。
// 来源:vLLM 0.5.0 / vllm/core/block_manager.py + vllm/worker/cache_engine
python
import torch
from collections import defaultdict
class PagedBlockManager:
def __init__(self, num_blocks, block_size=16, num_layers=80,
num_kv_heads=8, head_size=128, dtype=torch.float16):
self.block_size = block_size
self.num_layers = num_layers
# 物理块池,按GPU显存预分配
# 单block大小 = 2(K,V) × num_layers × block_size × num_kv_heads × head_size × dtype
block_bytes = 2 * num_layers * block_size * num_kv_heads * \
head_size * dtype.itemsize
# 预分配所有物理块到GPU,避免运行时申请
self.kv_cache = torch.empty(
num_blocks, 2, num_layers, block_size,
num_kv_heads, head_size, dtype=dtype, device='cuda'
)
# 空闲块池
self.free_blocks = list(range(num_blocks))
# 逻辑到物理的页表,每个序列一个映射
self.page_table = defaultdict(list)
def allocate(self, seq_id, seq_len):
"""为序列分配KV cache物理块"""
# 计算所需块数,向上取整
num_blocks_needed = (seq_len + self.block_size - 1) // self.block_size
if num_blocks_needed > len(self.free_blocks):
raise OOMError(
f"KV cache耗尽: 需要{num_blocks_needed}块,剩余{len(self.free_blocks)}块"
)
# 从空闲池取物理块,建立页表映射
physical_blocks = []
for _ in range(num_blocks_needed):
physical_blocks.append(self.free_blocks.pop())
self.page_table[seq_id] = physical_blocks
return physical_blocks
def append_token(self, seq_id, token_position, k, v):
"""追加一个token的KV到对应物理块"""
block_idx = token_position // self.block_size
offset = token_position % self.block_size
physical_block = self.page_table[seq_id][block_idx]
# 写入物理块的对应位置,跨层并行写入
self.kv_cache[physical_block, 0, :, offset] = k # K
self.kv_cache[physical_block, 1, :, offset] = v # V
def free(self, seq_id):
"""序列结束归还物理块到空闲池"""
for block in self.page_table[seq_id]:
self.free_blocks.append(block)
del self.page_table[seq_id]
def num_free_blocks(self):
return len(self.free_blocks)
def can_allocate(self, seq_len):
"""检查是否还能分配指定长度序列"""
needed = (seq_len + self.block_size - 1) // self.block_size
return needed <= len(self.free_blocks)
量化指标与边界
某项目落地PagedAttention后,KV cache碎片化彻底消除,显存利用率从60%提到95%,并发从15提到32(同卡同模型)。block_size经验值16,过小元数据(页表)开销大,过大显存浪费------block_size=4时页表占8%显存,block_size=16时占2%,block_size=64时占0.5%但单请求最短也要64token浪费。
边界与踩坑:PagedAttention的页表查询有开销,每attention前向需查页表定位物理块,vLLM通过kernel融合优化掉了这部分。block_size对长文本场景可调到32,对短对话保持16。分页管理增加了显存预分配的复杂度,需精确预估物理块数量------预估少了OOM,多了浪费显存。生产环境建议预留10%物理块作为突发缓冲。
2. 动态批fill/prefill冲突拖垮吞吐
痛点现场
某推理服务用连续批(continuous batching)调度,压测吞吐500 QPS,上线后实际只有180 QPS。排查发现调度策略按FCFS处理请求,一个长文本prefill请求(2000 token)到达后独占GPU做预填充,期间所有decode中的请求排队等待,decode延迟从50ms飙升到2秒。
prefill和decode的算力特征完全不同。prefill是计算密集,2000 token的prefill要跑一遍完整attention前向,GPU算力打满约800ms。decode是显存密集,每步只生成一个token,算力利用率低但需频繁读KV cache。两者混在同一批次调度,prefill阻塞decode,decode饿死prefill,互相拖累。
更严重的是batch组合不稳,调度器把长短请求混在一个batch,长请求拖慢整个batch的decode节奏------batch里有一个2000 token的请求,其他短请求也得等它decode完才能更新batch,吞吐被最慢请求拖垮。
工程方案:PD分离架构+优先级调度
PD分离的机理是把prefill和decode拆到不同节点池,prefill节点算力满载做预填充,decode节点显存优化做解码,两类负载互不阻塞。预填充完成后KV cache跨节点传到decode节点继续解码。
// 来源:vLLM 0.5.0 / vllm/core/scheduler.py + PD分离自研
python
import time
from enum import Enum
class RequestPhase(Enum):
PREFILL = "prefill"
DECODE = "decode"
class PDSeparatedScheduler:
def __init__(self, prefill_engine, decode_engine, kv_transfer_channel):
self.prefill_engine = prefill_engine # prefill专用推理引擎
self.decode_engine = decode_engine # decode专用推理引擎
self.kv_channel = kv_transfer_channel # KV跨节点传输通道
self.decode_queue = [] # decode中请求队列
self.prefill_queue = [] # 待预填充队列
def schedule(self):
"""两阶段独立调度,prefill与decode互不阻塞"""
# 阶段1:decode节点优先处理进行中请求,避免饿死
decode_batch = self._schedule_decode()
if decode_batch:
# 异步触发decode引擎前向,不阻塞prefill调度
self.decode_engine.run_batch_async(decode_batch)
# 阶段2:prefill节点独立处理新请求,与decode并行
prefill_batch = self._schedule_prefill()
if prefill_batch:
# prefill引擎前向,生成KV cache
prefill_results = self.prefill_engine.run_batch(prefill_batch)
# KV cache跨节点传输到decode引擎
for result in prefill_results:
self.kv_channel.transfer_async(
result.seq_id, result.kv_cache,
target_engine=self.decode_engine
)
# 传输完成后请求转入decode队列
self.decode_queue.append(result.seq_id)
def _schedule_decode(self):
"""decode调度:优先级队列,短请求优先避免拖慢batch"""
# 按剩余token数升序排,短请求先完成释放batch槽位
self.decode_queue.sort(key=lambda s: s.remaining_tokens)
batch = []
for seq in self.decode_queue:
if self.decode_engine.can_fit(seq):
batch.append(seq)
if len(batch) >= MAX_BATCH:
break
return batch
def _schedule_prefill(self):
"""prefill调度:按请求长度分桶,同batch内长度接近避免拖慢"""
# 长度分桶:[0-500], [500-1000], [1000-2000], [2000+]
buckets = defaultdict(list)
for seq in self.prefill_queue:
bucket_idx = seq.prompt_len // 500
buckets[bucket_idx].append(seq)
# 优先调度短桶,短请求prefill快释放算力
batch = []
for bucket_idx in sorted(buckets.keys()):
for seq in buckets[bucket_idx]:
if self.prefill_engine.can_fit(seq):
batch.append(seq)
if len(batch) >= MAX_PREFILL_BATCH:
break
if len(batch) >= MAX_PREFILL_BATCH:
break
# 从prefill队列移除已调度请求
for seq in batch:
self.prefill_queue.remove(seq)
return batch
def add_request(self, seq):
"""新请求入prefill队列"""
self.prefill_queue.append(seq)
def on_decode_done(self, seq_id):
"""decode完成的请求移出队列"""
self.decode_queue = [s for s in self.decode_queue if s.seq_id != seq_id]
量化指标与边界
某项目落地PD分离后,吞吐从180 QPS提到520 QPS,首token延迟从2秒压到300ms(prefill节点专用算力),decode延迟稳定在50ms。长度分桶让batch内请求长度接近,batch节奏从被最慢请求拖垮变成均衡。KV跨节点传输开销约8ms(10GB KV,万兆网络),占总延迟3%可接受。
边界与踩坑:PD分离需双倍节点,成本上升------但吞吐提升2.9倍抵消了成本,单QPS成本反而下降。跨节点KV传输是新瓶颈,万兆网络下10GB约8ms,超过10GB的长上下文场景需用RDMA或NVLink Fabric。长度分桶的桶边界需根据业务调,对话场景桶间距小(200 token),长文档场景桶间距大(1000 token)。decode优先调度短请求可能导致长请求饿死,需配aging机制------等待超时的长请求强制提优先级。
3. 量化长尾翻车:INT4在低频任务上掉点15%
痛点现场
某项目用GPTQ把Llama-70B从FP16量化到INT4,推理速度提升3倍,常见问题准确率只掉1%,团队认为成功上线。上线两周用户反馈某些专业问题答得离谱------这些是长尾问题,离线评测集没覆盖。补测发现INT4在长尾任务上掉点15%,金融专业术语识别错误率从3%飙到18%。
量化的 trade-off 被严重低估。团队只看平均指标,平均掉1%掩盖了长尾掉15%。根因是低比特表达范围不足,FP16有16位精度表示logit分布的细节,INT4只有16个离散值,长尾token的logit分布被压扁,模型在专业术语这种细粒度区分上失能。
更隐蔽的是量化敏感度不均匀。attention的Q/K投影层对量化敏感(影响attention分布),FFN层较鲁棒。统一量化所有层导致敏感层掉点严重,团队没做敏感度分析,一刀切INT4埋雷。
工程方案:敏感度感知混合精度量化
方案核心是逐层量化敏感度评估,敏感层(量化后loss变化大)保留FP16,鲁棒层量化INT4。敏感度用校准数据集计算量化前后loss相对变化,阈值0.05是经验值。
// 来源:AutoGPTQ 0.6.0 / auto_gptq/quantization/quantizer + 自研敏感度
python
import torch
import torch.nn.functional as F
class SensitivityAwareQuantizer:
def __init__(self, model, calibration_data, threshold=0.05):
self.model = model
self.calibration_data = calibration_data
self.threshold = threshold # 敏感度阈值,loss相对变化超5%保留FP16
self.sensitivity = {} # 每层敏感度记录
def evaluate_layer_sensitivity(self, layer_name, layer_module):
"""评估单层量化敏感度"""
# 1. 计算FP16基线loss
original_state = layer_module.state_dict()
fp_loss = self._compute_loss()
# 2. 临时量化该层到INT4
quantized_layer = self._quantize_layer(layer_module, bits=4)
self._replace_layer(layer_name, quantized_layer)
quant_loss = self._compute_loss()
# 3. 恢复原始FP16层
self._replace_layer(layer_name, layer_module)
layer_module.load_state_dict(original_state)
# 4. 敏感度 = loss相对变化
sensitivity = (quant_loss - fp_loss) / fp_loss
self.sensitivity[layer_name] = sensitivity
return sensitivity
def mixed_precision_quantize(self):
"""逐层评估并混合精度量化"""
# 长尾敏感度:校准数据需包含长尾样本,否则敏感度评估失真
for name, module in self.model.named_modules():
if self._is_quantizable(module):
sens = self.evaluate_layer_sensitivity(name, module)
print(f"层{name} 敏感度: {sens:.4f}", end=" ")
if sens > self.threshold:
# 敏感层保留FP16,不量化
print("保留FP16")
else:
# 鲁棒层量化INT4
quantized = self._quantize_layer(module, bits=4)
self._replace_layer(name, quantized)
print("量化INT4")
# 统计混合精度比例
fp16_layers = sum(1 for s in self.sensitivity.values()
if s > self.threshold)
total_layers = len(self.sensitivity)
print(f"\n混合精度: {fp16_layers}/{total_layers}层保留FP16"
f" ({fp16_layers/total_layers*100:.1f}%)")
def _compute_loss(self):
"""在校准数据上计算模型loss,含长尾样本"""
total_loss = 0
for batch in self.calibration_data:
with torch.no_grad():
outputs = self.model(batch.input_ids, labels=batch.labels)
total_loss += outputs.loss.item()
return total_loss / len(self.calibration_data)
def _quantize_layer(self, layer, bits=4):
"""GPTQ量化单层"""
# AutoGPTQ量化核心:基于Hessian矩阵的逐层量化
# 这里简化为量化接口调用
from auto_gptq import GPTQQuantizer
quantizer = GPTQQuantizer(bits=bits, group_size=128)
return quantizer.quantize_layer(layer)
def _replace_layer(self, name, new_layer):
"""替换模型中的层"""
parent, child = self._get_parent_child(name)
setattr(parent, child, new_layer)
def _is_quantizable(self, module):
"""判断是否为可量化的Linear层"""
return isinstance(module, torch.nn.Linear)
量化指标与边界
某项目落地混合精度后,平均准确率从INT4的掉1%改善到掉0.3%,长尾准确率从掉15%改善到掉2%,推理速度比纯FP16提升2.6倍(比纯INT4的3倍略慢,可接受)。敏感层通常占15-25%,主要是attention的Q/K投影和靠近输出的FFN层。
边界与踩坑:敏感度评估的校准数据必须含长尾样本,否则评估失真------某项目校准集全是常见问题,评估显示所有层都鲁棒,量化后长尾翻车。阈值0.05是经验,金融/医疗长尾场景建议0.02更保守。混合精度增加了模型存储复杂度(部分FP16部分INT4),推理引擎需支持异构精度加载。量化后必须做长尾专项评测,常见+长尾分两组指标分别看,不能只看平均。
4. 冷启动慢让弹性伸缩变成空谈
痛点现场
某推理服务配置了HPA(水平Pod自动伸缩),流量突增时触发扩容。压测显示扩容后3分钟新实例就绪接流量,上线后流量突增扩容,新实例5分钟才就绪------期间老实例被打爆,SLA违约。排查发现冷启动耗时主要在加载模型权重,70B模型权重140GB从镜像仓库拉到节点SSD要2分钟,从SSD加载到GPU显存要1分钟,加上容器初始化5分钟才就绪。
冷启动慢让弹性伸缩失效,流量突增时新实例来不及就绪,只能靠超配资源应对峰值------平时闲置浪费,峰值时仍可能不够。某项目为应对峰值超配3倍资源,平时利用率不足20%,成本爆炸。
工程方案:权重本地缓存+共享内存零拷贝
方案分两层。缓存层:权重预拉到节点本地SSD,新实例启动时优先读本地缓存,避免每次从镜像仓库拉。加载层:用mmap把权重文件映射到内存,再通过CUDA的host→device零拷贝加载到显存,省掉内核态到用户态的拷贝。
// 来源:triton-inference-server 23.10 + 自研零拷贝
python
import mmap
import torch
import os
import time
class FastColdStartLoader:
def __init__(self, weight_cache_dir="/var/cache/model_weights"):
self.cache_dir = weight_cache_dir
def load_model_fast(self, model_name, weight_path, target_gpu=0):
"""快速冷启动加载:本地缓存+mmap零拷贝"""
start = time.time()
# 1. 本地缓存检查,命中则跳过镜像仓库拉取
cached_path = os.path.join(self.cache_dir, model_name)
if not os.path.exists(cached_path):
# 未命中:从镜像仓库预拉到本地SSD缓存
self._prefetch_to_cache(weight_path, cached_path)
prefetch_time = time.time() - start
print(f"权重预拉耗时: {prefetch_time:.1f}s")
else:
print("权重本地缓存命中,跳过预拉")
# 2. mmap映射权重文件到内存,避免内核→用户态拷贝
weight_mmap = self._mmap_weights(cached_path)
# 3. 从mmap直接构建tensor,零拷贝
tensors = self._mmap_to_tensors(weight_mmap, model_name)
# 4. 加载到GPU显存,用pinned memory加速H2D传输
model = self._load_to_gpu(tensors, target_gpu)
elapsed = time.time() - start
print(f"冷启动总耗时: {elapsed:.1f}s")
return model
def _prefetch_to_cache(self, src, dst):
"""从镜像仓库预拉权重到本地SSD,后台预热"""
# 生产环境用对象存储并发分片下载,140GB约90秒(千兆网络)
# 这里简化为文件拷贝
import shutil
os.makedirs(self.cache_dir, exist_ok=True)
shutil.copy(src, dst)
def _mmap_weights(self, path):
"""mmap映射权重文件到进程地址空间"""
f = open(path, 'rb')
# mmap映射,ACCESS_READ避免写时复制开销
mm = mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ)
return mm
def _mmap_to_tensors(self, mm, model_name):
"""从mmap构建tensor,按权重布局解析"""
# 假设权重已按层存储,每层有shape元数据
metadata = self._load_metadata(model_name)
tensors = {}
offset = 0
for name, (shape, dtype) in metadata.items():
numel = 1
for s in shape:
numel *= s
# 从mmap构建tensor,零拷贝
tensor = torch.frombuffer(
mm, dtype=dtype, count=numel, offset=offset
).reshape(shape)
tensors[name] = tensor
offset += numel * dtype.itemsize
return tensors
def _load_to_gpu(self, tensors, target_gpu):
"""加载tensors到GPU,用pinned memory加速"""
model = self._init_empty_model(target_gpu)
for name, tensor in tensors.items():
# 分配pinned memory,加速H2D传输
pinned = torch.empty_like(tensor).pin_memory()
pinned.copy_(tensor)
# 异步传输到GPU,多层并行加载
gpu_tensor = pinned.to(
device=f'cuda:{target_gpu}', non_blocking=True
)
# 写入模型对应参数
param = model.get_parameter(name)
param.data.copy_(gpu_tensor)
return model
# 预热脚本:节点启动时预拉所有模型权重到本地缓存
def warmup_node(model_list):
"""节点启动时预拉权重,后续实例冷启动直接命中缓存"""
loader = FastColdStartLoader()
for model in model_list:
cached = os.path.join(loader.cache_dir, model.name)
if not os.path.exists(cached):
loader._prefetch_to_cache(model.remote_path, cached)
print(f"预热完成: {model.name}")
量化指标与边界
某项目落地零拷贝冷启动后,70B模型冷启动从5分钟压到12秒------本地缓存命中8秒(mmap+零拷贝加载),未命中12秒(含预拉)。HPA扩容实效,流量突增时新实例15秒就绪接流量,SLA违约清零。节点预热在集群扩容时提前拉权重,实例冷启动全部命中缓存。
边界与踩坑:本地缓存占用SSD空间,140GB权重×多模型易撑爆节点SSD,需配权重LRU清理。mmap对大文件(>10GB)有page fault开销,首次访问时按页加载,需预热时强制顺序读触发预读。pinned memory占用是不可换出的物理内存,过多导致系统OOM,需限制并发加载的模型数。节点预热增加集群启动时间,需权衡------预热全模型 vs 按需拉取,流量峰值频繁的集群选预热,平稳的选按需。
5. 多租户混部无隔离导致互相抢算力
痛点现场
某AI平台多个业务团队共用推理集群,租户A是内部客服(低频高实时),租户B是离线批处理(高频低实时)。批处理任务把GPU算力占满80%,客服请求排队等待,首token延迟从200ms飙到3秒,用户挂线率激增。
多租户混部的根因是GPU资源无公平调度,先到先得导致大租户饿死小租户。某项目用Docker做租户隔离,但Docker只隔离CPU/内存不隔离GPU,GPU算力仍被先到请求抢满。团队尝试按租户分配固定GPU,但导致资源利用率低------客服租户的GPU平时闲置不能给批处理用,批处理租户峰值时又不够。
工程方案:令牌桶配额+优先级队列
方案分两层。配额层:每租户令牌桶限流,按SLA分配配额,超额请求排队。优先级层:高优先级租户突发可临时抢占低优先级配额,低优先级请求回退队列等待。
// 来源:NVIDIA Triton 23.10 + 自研令牌桶
python
import time
import threading
from collections import defaultdict, deque
class MultiTenantTokenBucket:
def __init__(self, tenant_configs):
"""
tenant_configs: {
"tenant_a": {"capacity": 100, "refill_rate": 50, "priority": 1},
"tenant_b": {"capacity": 500, "refill_rate": 300, "priority": 2}
}
priority越小越高,1高于2
"""
self.tenants = tenant_configs
self.tokens = {
t: c["capacity"] for t, c in tenant_configs.items()
}
self.last_refill = {t: time.time() for t in tenant_configs}
self.queues = defaultdict(deque) # 每租户排队请求
self.lock = threading.Lock()
def acquire(self, tenant, cost):
"""租户请求获取算力配额"""
with self.lock:
# 按速率补充令牌
now = time.time()
elapsed = now - self.last_refill[tenant]
self.tokens[tenant] = min(
self.tenants[tenant]["capacity"],
self.tokens[tenant] + elapsed *
self.tenants[tenant]["refill_rate"]
)
self.last_refill[tenant] = now
# 令牌充足直接放行
if self.tokens[tenant] >= cost:
self.tokens[tenant] -= cost
return True
# 令牌不足,尝试优先级抢占
if self._try_preempt(tenant, cost):
return True
# 抢占失败,请求入队等待
self.queues[tenant].append((cost, time.time()))
return False
def _try_preempt(self, tenant, cost):
"""高优先级租户突发抢占低优先级配额"""
my_priority = self.tenants[tenant]["priority"]
# 找低优先级(priority数值更大)租户的富余令牌
for other, other_config in self.tenants.items():
if other_config["priority"] <= my_priority:
continue # 同级或更高优先级,不抢占
if self.tokens[other] >= cost:
# 抢占低优先级租户令牌
self.tokens[other] -= cost
print(f"租户{tenant}抢占{other}令牌{cost}")
return True
return False
def release(self, tenant, cost):
"""请求完成释放令牌(用于抢占的令牌归还)"""
with self.lock:
# 抢占的令牌归还给原租户
# 简化实现:归还被抢占租户
pass
def process_queues(self):
"""后台线程处理排队请求,令牌补充后依次放行"""
while True:
time.sleep(0.01) # 10ms检查一次
with self.lock:
for tenant, queue in self.queues.items():
if not queue:
continue
# 重新补充令牌
now = time.time()
elapsed = now - self.last_refill[tenant]
self.tokens[tenant] = min(
self.tenants[tenant]["capacity"],
self.tokens[tenant] + elapsed *
self.tenants[tenant]["refill_rate"]
)
self.last_refill[tenant] = now
# 队首请求令牌够则放行
if queue and self.tokens[tenant] >= queue[0][0]:
cost, _ = queue.popleft()
self.tokens[tenant] -= cost
# 触发该请求执行
self._execute(tenant, cost)
量化指标与边界
某平台落地令牌桶+优先级后,客服租户首token延迟稳定在200ms内(优先级1,突发可抢占),批处理租户吞吐受限于配额但平均延迟可接受(优先级2,排队等待)。GPU利用率从按租户分割的40%提到混部的75%,成本下降。capacity配置是关键,客服租户capacity=100(峰值QPS)、refill_rate=50(平均QPS),批处理capacity=500、refill_rate=300。
边界与踩坑:令牌桶的trade-off是容量过大失去隔离(大租户突发挤占小租户),过小突发请求被限。capacity建议=峰值QPS×最大token数,refill_rate=平均QPS。优先级抢占可能导致低优先级租户长期饿死,需配aging------排队超时的低优先级请求强制提优先级。令牌桶只管算力配额不管显存隔离,多租户GPU显存仍可能互相OOM,需配合显存配额或分卡部署。
总结
推理部署层的本质是显存账本与调度博弈。PagedAttention分页KV消除碎片把显存利用率从60%提到95%,PD分离让prefill与decode互不阻塞把吞吐提2.9倍,敏感度感知量化在保速度的同时压住长尾掉点,零拷贝冷启动把弹性伸缩从空谈变成15秒就绪,令牌桶+优先级让多租户混部既能共享资源又不互相饿死。五个支点都有量化指标与边界,落地顺序建议:KV cache与冷启动先行(保服务能跑起来),PD分离与量化提吞吐,多租户隔离作为平台化终局。