推理部署层:吞吐上不去的显存账本与调度博弈

摘要

推理服务上线即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

graph LR A[逻辑序列1] --> B[逻辑块0] A --> C[逻辑块1] A --> D[逻辑块2] E[逻辑序列2] --> F[逻辑块0] E --> G[逻辑块1] H[物理块池] --> I[物理块3] H --> J[物理块7] H --> K[物理块12] B -.映射.-> I C -.映射.-> J D -.映射.-> K F -.映射.-> 物理块5 G -.映射.-> 物理块9

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分离架构+优先级调度

flowchart LR A[请求队列] --> B{调度器} B -->|新请求| C[Prefill节点池] B -->|decode中| D[Decode节点池] C --> E[预填充计算KV cache] E -->|KV传输| D D --> F[逐token解码] F --> G[输出] F -->|序列未完| D C -.算力独立.-> D

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埋雷。

工程方案:敏感度感知混合精度量化

flowchart TD A[FP16模型] --> B[逐层敏感度评估] B --> C{敏感度阈值判定} C -->|大于0.05| D[敏感层保留FP16] C -->|小于0.05| E[鲁棒层量化INT4] D --> F[混合精度模型] E --> F F --> G[长尾专项评测] G -->|达标| H[上线] G -->|不达标| I[阈值收紧重做]

方案核心是逐层量化敏感度评估,敏感层(量化后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%,成本爆炸。

工程方案:权重本地缓存+共享内存零拷贝

flowchart LR A[新实例启动] --> B{权重本地缓存命中} B -->|命中| C[共享内存读取] B -->|未命中| D[镜像仓库拉取] D --> E[写入本地SSD缓存] E --> C C --> F[mmap映射显存] F --> G[GPU就绪] G --> H[接流量]

方案分两层。缓存层:权重预拉到节点本地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平时闲置不能给批处理用,批处理租户峰值时又不够。

工程方案:令牌桶配额+优先级队列

graph TD A[GPU资源池] --> B[令牌桶调度器] C[租户A高优先级] --> B D[租户B低优先级] --> B B -->|配额内| E[租户A请求执行] B -->|配额内| F[租户B请求执行] B -->|超配额| G[租户B请求排队] E --> H[GPU算力] F --> H B -->|优先级抢占| I[租户A突发优先] I --> H

方案分两层。配额层:每租户令牌桶限流,按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分离与量化提吞吐,多租户隔离作为平台化终局。

相关推荐
OpenCloudOS2 小时前
OpenCloudOS 理事会更新 | 腾讯云、海光助力建设 AI Infra 开源开放底座
人工智能·开源·腾讯云
私人珍藏库2 小时前
[Android] 一木百宝箱-万能百宝箱+抖音去水印等几百种功能
android·人工智能·app·软件·多功能
没落英雄2 小时前
3. DeepAgents 实战 - Memory Skills 与上下文工程
前端·人工智能·架构
Bigger2 小时前
对不起!我错怪你了,UnoCSS:一次和 AI 一起排查 DevTools 卡顿的经历
前端·css·人工智能
CaffeinePro2 小时前
Langchain环境搭建与LCEL核心语法
人工智能·agent
不懂的浪漫2 小时前
Andrew Ng(吴恩达)谈 Loop Engineering:AI 编程中的三个关键循环
人工智能
人工智能培训2 小时前
大模型驱动下传统大数据架构的变革方向
大数据·人工智能·重构·架构·agent·agi
一次旅行2 小时前
AI 前沿日报 | 2026年7月6日
人工智能
AI科技星2 小时前
全域谱分析:无穷维超复数信息场分形统一场论——自然、量子、金融多重分形第一性原理完整体系(中英双语终稿)
人工智能·python·算法·金融·乖乖数学·全域数学