大模型多租户推理架构原理深度解析:会话ID管理、算力分配算法与Golang工程实践(二)

大模型多租户推理架构原理深度解析:会话ID管理、算力分配算法与Golang工程实践(二)

大模型时代的算力焦虑与多租户挑战
自2022年底大语言模型(LLM)迎来爆发式增长以来,AI基础设施的焦点逐渐从“如何训练出更大的模型”转移到“如何高效、低成本地推理和部署模型”。在实际的商业化落地中,大模型服务通常以SaaS(Software as a Service)或MaaS(Model as a Service)的形式提供给成百上千个不同的企业或个人用户。这就引入了多租户(Multi-tenancy) 场景。

在多租户场景下,大模型推理面临着前所未有的挑战:

  1. 资源争抢与公平性:不同租户的流量特征差异巨大(如长文本分析 vs 短文本对话),如何保证高优先级租户的SLA(服务等级协议),同时避免低优先级租户饿死?
  2. 显存墙与算力墙:大模型推理是典型的访存密集型(Memory-bound)任务,KV Cache的显存占用往往远超模型权重本身。如何在有限的GPU显存中最大化吞吐量?
  3. 上下文管理与链路追踪:在分布式、高并发的流式请求中,如何精准管理每个会话的生命周期、计费以及全链路追踪?

本文将深入探讨大模型多租户推理系统的两大核心命题:会话ID(Session/Request ID)管理模型算力分配算法。我们将以国产大模型明星DeepSeek为例,剖析其底层架构与推理优化策略,并横向对比OpenAI、阿里云、腾讯云、百度等多家主流厂商的实现方式及其优劣。最后,本文将提供一套基于Golang实现的完整多租户大模型调用网关案例,将理论转化为可落地的工程实践。


第一部分:大模型会话ID(Session ID)的深度解析

在传统的微服务架构中,Request ID(请求ID)主要用于链路追踪。但在大模型推理系统中,Session ID(会话ID)Request ID(请求ID) 被赋予了更核心的业务与系统级意义。

1.1 会话ID的生命周期与核心作用

在大模型API调用中,一个完整的交互通常包含多次Request。Session ID用于将这些离散的Request串联成一个具有逻辑上下文的Session。

1. 上下文关联与KV Cache管理

大模型是自回归(Auto-Regressive)模型,生成第 $N$ 个Token需要依赖前 $N-1$ 个Token的注意力计算结果。为了避免重复计算,系统会将中间结果缓存为 KV Cache

  • Session ID的作用:推理引擎(如vLLM、TensorRT-LLM)通过Session ID来标识和管理KV Cache。当同一个Session的后续Request到来时,引擎通过Session ID直接加载历史KV Cache,从而将Prefill(预填充)阶段的计算量降至最低。
  • Prefix Caching(前缀缓存):现代推理引擎支持基于Session ID的Prefix Caching。如果多个Session共享相同的System Prompt,引擎可以通过Session ID的哈希匹配,复用这部分KV Cache,极大节省显存和算力。

2. 计费与Token统计

大模型的计费通常基于Input Token和Output Token。

  • Session ID是计费的锚点。网关层通过Session ID记录整个会话的Token消耗,支持按Session进行账单聚合、配额扣减和超额熔断。

3. 链路追踪与可观测性

大模型推理链路长,涉及网关、路由、推理节点、存储等多个组件。

  • 将Session ID与Trace ID(如OpenTelemetry标准)绑定,可以实现从用户发起请求到模型生成最后一个Token的全链路追踪。当出现首字延迟(TTFT)过高或生成中断时,运维人员可以通过Session ID快速定位是网络问题、排队问题还是GPU OOM(显存溢出)问题。

4. 安全与合规审计

在多租户场景下,内容安全至关重要。Session ID使得安全审计系统能够还原完整的对话上下文,而不是孤立地审查单条Request,从而更准确地识别越狱攻击(Jailbreak)或恶意诱导。

1.2 会话ID的生成算法选型

生成Session ID需要满足全局唯一性、单调递增性(可选,利于数据库索引)、高性能和防碰撞。

算法原理优点缺点适用场景
UUID v4基于随机数生成128位字符串完全去中心化,无需协调字符串长,无序导致B+树索引页分裂,性能差对性能要求不高的内部系统
Snowflake时间戳+机器ID+序列号(64位整型)趋势递增,性能极高,占用空间小依赖机器时钟,需解决时钟回拨问题高并发、对数据库索引友好的核心系统
ULID48位时间戳+80位随机数(Crockford Base32编码)单调递增,兼容UUID格式,无时钟回拨问题字符串较长(26字符)需要兼顾UUID兼容性和递增性的场景
NanoID基于URL安全字符集的随机字符串极短,可自定义字母表纯随机,无序前端或对外暴露的短链接/会话标识

工程建议:在大模型网关内部流转,推荐使用 Snowflake 生成的 int64 作为内部Session ID,以保证极高的路由和缓存查找性能;对外暴露给客户端时,可将其转换为 ULID 或带业务前缀的字符串(如 sess_xxx)。


第二部分:大模型算力分配算法的演进与核心原理

大模型推理的算力分配与传统CPU服务的调度有着本质区别。CPU调度主要关注CPU时间片的分配,而GPU推理调度则是在计算单元(CUDA Cores/Tensor Cores)显存(HBM) 之间走钢丝。

2.1 显存墙:KV Cache与计算单元的博弈

假设部署一个70B参数的模型,使用FP16精度,模型权重本身需要约140GB显存。如果一张A100 80GB显卡,仅加载权重就占用了近两张卡。
当并发请求增加时,每个请求都需要分配KV Cache。对于上下文长度为4K的请求,70B模型的KV Cache大约需要1.5GB。这意味着,即使不考虑计算,仅分配几十个并发请求的显存,就会导致GPU OOM。

算力分配的核心矛盾:GPU的算力(TFLOPS)往往处于闲置状态,因为显存被KV Cache占满,无法接收新的Request。这就是著名的“显存墙”。

2.2 算力分配算法的演进

为了打破显存墙,最大化GPU算力利用率,业界演化出了以下几代算力分配算法:

1. Static Batching(静态批处理)

  • 原理:收集一批Request,凑齐一个Batch后,一起送入GPU计算。必须等Batch中所有Request都生成完毕(遇到EOS),才能释放显存并接收新Request。
  • 劣势:木桶效应严重。如果一个短请求10个Token就结束,而长请求需要1000个Token,短请求释放的显存和算力只能闲置等待,GPU利用率极低。

2. Continuous Batching(连续批处理 / 动态批处理)

  • 原理:以 Iteration(迭代/步) 为粒度进行调度。在每一个Decode步,调度器检查当前Batch中是否有Request已经生成完毕。如果有,立即将其踢出Batch,释放其KV Cache显存,并从等待队列中拉入新的Request填补空位。
  • 优势:彻底解决了Static Batching的木桶效应,GPU算力利用率提升数倍。这是目前vLLM、TensorRT-LLM等主流框架的标配。

3. PagedAttention:显存碎片化的终结者

  • 原理:借鉴操作系统的虚拟内存分页机制。KV Cache不再需要连续的显存空间,而是被切分成固定大小的Block(如16个Token为一个Block)。通过Block Table(页表)进行逻辑到物理显存的映射。
  • 优势:几乎消除了显存外部碎片,内部碎片控制在极小范围。使得系统可以接纳更多的并发Request,吞吐量提升2-4倍。

4. Prefill与Decode解耦调度(Disaggregated Serving)

  • 原理:大模型推理分为Prefill(处理Prompt,计算密集型)和Decode(逐个生成Token,访存密集型)。两者的最优Batch Size和硬件需求完全不同。Splitwise、DistServe等架构将Prefill和Decode拆分到不同的GPU集群上执行。
  • 优势:避免了长Prompt请求在Decode阶段阻塞短请求,显著降低TTFT(首字延迟)和TPOT(每个Token的生成延迟)。

2.3 多租户环境下的算力分配策略

在多租户场景下,单纯的吞吐量最大化是不够的,必须引入资源隔离与公平性保障

  1. 物理隔离(Dedicated Clusters):为VIP租户分配专属的GPU节点。
    • 优点:绝对的SLA保障,互不干扰。
    • 缺点:资源利用率极低,成本高昂。
  2. 逻辑隔离(Shared Clusters with Quotas):所有租户共享GPU资源池,通过调度算法进行软隔离。
    • 基于权重的令牌桶:为每个租户分配TPM(Tokens Per Minute)和RPM(Requests Per Minute)配额。
    • 优先级抢占:当GPU资源紧张时,调度器根据租户优先级,暂停低优先级租户的Decode过程(将其KV Cache Swap到CPU内存),优先保障高优先级租户。

第三部分:以DeepSeek为例的架构剖析

DeepSeek(深度求索)作为国产大模型的佼佼者,其V2/V3模型在架构设计和推理优化上做出了大量创新,为多租户算力分配提供了极佳的参考样本。

3.1 DeepSeek-V3的MoE架构对算力分配的挑战

DeepSeek-V3采用了 MoE(Mixture of Experts,混合专家) 架构,包含256个路由专家(Routed Experts),每个Token仅激活8个专家。同时,它引入了 MLA(Multi-head Latent Attention) 机制。

这种架构对算力分配提出了特殊要求:

  1. 专家负载均衡:如果路由算法导致某些专家被频繁激活,而另一些专家闲置,会导致严重的计算倾斜和通信瓶颈。
  2. 通信开销:在分布式部署中,Token需要在不同的GPU节点间进行All-to-All通信,以路由到对应的专家节点。通信延迟可能成为算力分配的短板。

3.2 DeepSeek推理框架的算力分配优化

针对上述挑战,DeepSeek在推理端(如DeepSeek-Infer)采用了以下算力分配与调度策略:

1. 细粒度流水线并行与DualPipe算法

为了掩盖MoE架构中All-to-All通信的延迟,DeepSeek提出了 DualPipe 算法。

  • 原理:将流水线并行(Pipeline Parallelism)的微批次(Micro-batch)进一步拆分,实现前向传播和反向传播(在推理中对应Prefill和Decode的不同阶段)的交替执行。通过精心设计的调度时间表,使得计算操作与通信操作在时间上完全重叠(Overlap)。
  • 效果:在几乎不增加显存占用的情况下,将通信延迟隐藏在计算时间内,大幅提升了GPU的有效算力利用率。

2. MLA与KV Cache压缩

DeepSeek-V3的MLA机制通过将Query和Key的投影压缩到一个低秩的潜在空间(Latent Space),极大地减少了KV Cache的体积。

  • 对算力分配的意义:KV Cache体积的缩小,意味着在相同的GPU显存下,可以容纳更大的Batch Size,或者支持更长的上下文(如128K)。这使得多租户调度器在分配显存时更加从容,能够接纳更多并发会话。

3. 辅助损失(Auxiliary Loss)与动态路由

为了解决专家负载均衡问题,DeepSeek在训练和推理时引入了辅助损失函数,惩罚负载过高的专家。在推理阶段,调度器会实时监控各专家的负载情况,动态调整路由策略,确保算力在各个专家节点间均匀分配。

3.3 DeepSeek API的会话ID与上下文管理实践

在DeepSeek的官方API中,会话ID的管理体现了极高的工程水准:

  • 流式响应(SSE)与Session绑定:DeepSeek的流式接口中,每个Chunk都携带隐式的上下文状态。网关层通过Session ID维护流式连接的状态机,处理断线重连和上下文续传。
  • 长上下文的高效利用:得益于MLA和PagedAttention的底层优化,DeepSeek API在处理128K长文本时,依然能保持较低的TTFT。其底层通过Session ID精准定位Prefix Cache,避免了重复计算。

第四部分:多家主流厂商实现方式剖析与优劣对比

大模型推理基础设施是一个高度复杂的系统工程。国内外的头部云厂商和AI公司都在此投入了重兵。以下我们剖析OpenAI、阿里云、腾讯云、百度四家代表性厂商的实现方式及其优劣。

4.1 OpenAI:极致的体验与全局调度

OpenAI作为大模型的商业先驱,其推理基础设施(内部代号可能为各类自研系统,底层基于高度定制的Triton和vLLM/TensorRT)代表了业界的最高水平。

  • 核心实现
    • Tiered Batching(分层批处理):OpenAI将请求分为Prefill和Decode两个池子。Prefill池专注于快速处理Prompt,降低TTFT;Decode池专注于高吞吐生成。
    • 全局Prefix Caching:通过全局的Radix Tree(基数树)管理所有Session的KV Cache。只要System Prompt或前缀相同,无论属于哪个租户,都能实现秒级复用。
    • 动态算力分配:基于强化学习的全局调度器,实时预测流量峰谷,动态调整各个模型实例的Batch Size和显存分配。
  • 优势
    • 极致的TTFT和TPS(Tokens Per Second)体验。
    • 极高的资源利用率,Prefix Caching大幅降低了算力成本。
  • 劣势
    • 系统极度复杂,闭源且难以被中小厂商复制。
    • 多租户隔离主要依赖严格的Rate Limit(限流),在极端突发流量下,仍可能出现排队现象。

4.2 阿里云(通义千问/PAI-EAS):云原生Serverless与弹性调度

阿里云依托其强大的云原生基础设施,在通义千问的部署上主打弹性Serverless

  • 核心实现
    • PAI-EAS弹性推理:基于Knative和自研的GPU调度器,实现推理实例的秒级弹性伸缩。当流量突增时,自动拉起新的GPU节点;流量低谷时,缩容至零(或保留最小副本)。
    • 底层框架优化:深度定制vLLM,引入BladeLLM等自研加速库,针对阿里云的异构硬件(如含光NPU、不同型号的GPU)进行算子级优化。
    • 多租户资源池化:通过ACK(容器服务)的GPU共享和隔离技术(cGPU),实现单卡多租户逻辑隔离,提升长尾小客户的资源利用率。
  • 优势
    • 弹性能力极强,完美契合SaaS应用流量波动大的特点。
    • 按量付费(Pay-as-you-go),大幅降低中小企业的试错成本。
  • 劣势
    • 弹性伸缩存在冷启动延迟(尽管在优化,但加载几十GB的模型权重仍需时间)。
    • 跨节点的KV Cache迁移成本较高,弹性扩容后的新节点无法立即享受Prefix Caching的红利。

4.3 腾讯云(混元/Angel Inference):分布式KV Cache与异构调度

腾讯混元大模型背后是腾讯AI Lab和腾讯云联合打造的 Angel Inference 推理框架。

  • 核心实现
    • 分布式KV Cache管理:Angel引入了全局的分布式KV Cache池。当单节点显存不足时,可以将KV Cache无缝迁移到CPU内存或其他GPU节点,甚至利用RDMA网络进行高速传输。
    • 异构算力统一调度:腾讯内部拥有大量的异构GPU(如T4, A10, H800等)。Angel调度器能够根据模型大小和请求特征,智能地将请求路由到最合适的硬件上。
    • 显存池化与超卖:通过精细的显存超卖策略,在保障SLA的前提下,最大化单卡部署的并发数。
  • 优势
    • 长文本支持能力突出,分布式KV Cache有效解决了单机显存瓶颈。
    • 多租户资源池化做得非常深,硬件兼容性好。
  • 劣势
    • 分布式KV Cache引入了额外的网络通信开销,对网络带宽和延迟要求极高。
    • 系统复杂度高,运维门槛高。

4.4 百度(文心一言/飞桨):端到端软硬协同与国产化适配

百度文心一言是国内最早商业化落地的大模型,其背后是飞桨(PaddlePaddle)框架的端到端优化。

  • 核心实现
    • 端到端软硬协同:百度不仅做软件,还自研了昆仑芯(AI芯片)。飞桨框架针对昆仑芯和NVIDIA GPU进行了深度的算子融合和底层优化。
    • 4D混合并行:在推理端,飞桨支持数据并行、张量并行、流水线并行和专家并行(针对MoE)的灵活组合,算力分配策略极其丰富。
    • 量化与蒸馏:百度在推理端大规模应用了INT8/INT4量化和模型蒸馏技术,在不明显损失精度的情况下,将显存占用和计算量减半。
  • 优势
    • 国产化适配最好,在信创场景下具有不可替代的优势。
    • 整体吞吐量高,量化技术使得单卡部署成本大幅降低。
  • 劣势
    • 生态相对封闭,主要服务于百度内部及百度云客户,开源社区的活跃度不及vLLM等纯开源项目。
    • 对非百度自研硬件的优化力度相对较弱。

4.5 横向对比总结

维度OpenAI阿里云 (PAI-EAS)腾讯云 (Angel)百度 (飞桨)
核心优势极致延迟与体验,全局调度云原生弹性,Serverless分布式KV Cache,长文本软硬协同,国产化,高吞吐
算力分配策略分层批处理,强化学习调度弹性伸缩,单卡多租隔离显存池化,异构路由4D混合并行,深度量化
多租户隔离严格的Rate Limit容器级/单卡级逻辑隔离显存超卖与池化队列优先级与配额管理
适用场景追求极致体验的全球级SaaS流量波动大、追求性价比的SaaS长文本、复杂企业级应用信创场景、私有化部署、高吞吐

第五部分:多租户大模型算力分配算法设计

在理解了底层原理和厂商实践后,我们需要设计一套适用于通用大模型网关的多租户算力分配算法。该算法需要兼顾公平性、资源利用率和SLA保障。

5.1 算法架构分层

我们将算力分配算法分为三层:

  1. 接入层(Access Layer):负责会话ID管理、租户鉴权、基于令牌桶的粗粒度限流(RPM/TPM)。
  2. 路由层(Routing Layer):负责智能调度,根据后端GPU节点的实时状态(显存利用率、队列深度、算力负载),将请求路由到最优节点。
  3. 推理层(Inference Layer):负责Continuous Batching、PagedAttention、优先级抢占等微观算力分配。

5.2 路由层:基于多维状态的加权调度算法

在路由层,简单的轮询(Round Robin)或随机算法无法满足大模型推理的需求。我们设计一种基于多维状态的加权调度算法(Multi-dimensional State-based Weighted Scheduling)

1. 节点状态采集

每个推理节点(如运行vLLM的实例)定期向注册中心上报以下指标:

  • $M_{util}$:GPU显存利用率(0~1)。
  • $Q_{len}$:当前等待队列的长度(Pending Requests)。
  • $G_{util}$:GPU计算单元利用率(SM Utilization)。
  • $C_{cap}$:节点的最大并发容量(由硬件和模型大小决定)。

2. 节点健康度评分(Health Score)

计算每个节点的健康度评分 $S_i$,评分越低,代表节点越空闲,越应该接收新请求。

$$ S_i = w_1 \cdot \max(0, M_{util} - \theta_m) + w_2 \cdot \frac{Q_{len}}{C_{cap}} + w_3 \cdot G_{util} $$

其中:

  • $\theta_m$ 是显存利用率的安全阈值(如0.9)。当显存利用率超过阈值时,惩罚项急剧增加,防止节点OOM。
  • $w_1, w_2, w_3$ 是权重系数,可根据业务侧重点调整(如侧重降低延迟,则增大 $w_2$)。

3. 租户优先级加权

当多个租户同时请求时,引入租户优先级 $P_t$(如VIP租户 $P_t=1.0$,普通租户 $P_t=0.5$)。
最终路由得分 $R_{i,t} = S_i - \alpha \cdot P_t$。
选择 $R_{i,t}$ 最小的节点 $i$ 处理租户 $t$ 的请求。这保证了高优先级租户在资源紧张时,能够被路由到负载更低的节点,甚至触发低优先级租户的抢占。

5.3 推理层:基于优先级的抢占式Continuous Batching

在推理节点内部,当显存即将耗尽时,调度器需要执行抢占(Preemption):

  1. Swap(交换):将低优先级租户的KV Cache从GPU显存Swap到CPU内存。当该租户重新获得资源时,再Swap回来。
  2. Recompute(重计算):如果CPU内存也满了,直接丢弃低优先级租户的KV Cache,在下次调度时重新进行Prefill计算。

通过这种多层级的算力分配算法,我们可以在保证核心租户SLA的同时,最大化整体GPU资源的利用率。


6.1 宏观技术架构流程图

该图展示了系统的五大核心层级,重点突出了 Session ID 的生成节点以及推理引擎内部如何通过 PagedAttention 实现 KV Cache 的物理隔离。

flowchart TD
    classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px;
    classDef client fill:#e1f5fe,stroke:#01579b,stroke-width:2px,color:#000;
    classDef gateway fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000;
    classDef router fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,color:#000;
    classDef engine fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000;
    classDef memory fill:#ffebee,stroke:#b71c1c,stroke-width:2px,color:#000;
    classDef highlight fill:#fff9c4,stroke:#fbc02d,stroke-width:3px,color:#000;

    subgraph ClientLayer["1. 客户端层 (Client Layer)"]
        C1["租户应用 / 终端用户"]:::client
    end

    subgraph GatewayLayer["2. API 网关层 (Gateway Layer)"]
        G1["租户鉴权 (Auth & Tenant ID)"]:::gateway
        G2["生成 Session ID (Snowflake/ULID)"]:::highlight
        G3["多租户限流 (RPM/TPM Token Bucket)"]:::gateway
    end

    subgraph RouterLayer["3. 路由与调度层 (Routing & Scheduling)"]
        R1["节点状态采集 (Mem/Queue/SM)"]:::router
        R2["多维加权调度算法"]:::router
        R3["优先级路由与负载均衡"]:::router
    end

    subgraph EngineLayer["4. 推理引擎层 (Inference Engine - vLLM/TRT)"]
        E1["请求解析与 Prefill 计算"]:::engine
        E2["Continuous Batching 调度器"]:::engine
        E3["Context Manager (上下文管理器)"]:::highlight
        E4["Prefix Caching (Radix Tree 前缀缓存)"]:::engine
    end

    subgraph MemoryLayer["5. 模型内部上下文隔离 (GPU Memory Isolation)"]
        M1["Block Table (逻辑页表映射)"]:::memory
        M2["PagedAttention 物理显存池"]:::highlight
        M3["租户 A 的 KV Cache (Session 1)"]:::memory
        M4["租户 B 的 KV Cache (Session 2)"]:::memory
    end

    C1 -- "1. 发起请求 (携带 API Key)" --> G1
    G1 -- "2. 提取 Tenant ID" --> G2
    G2 -- "3. 生成全局唯一 Session ID" --> G3
    G3 -- "4. 配额检查通过" --> R1
    
    R1 -- "5. 获取实时负载" --> R2
    R2 -- "6. 计算节点健康度" --> R3
    R3 -- "7. 路由至最优 GPU 节点" --> E1

    E1 -- "8. 提取 Prompt 特征" --> E4
    E4 -- "9. 查找/复用历史 KV Cache" --> E3
    E3 -- "10. 分配逻辑 Block ID" --> M1
    M1 -- "11. 映射至物理显存 Block" --> M2
    
    M2 -- "12. 严格物理隔离" --> M3
    M2 -- "12. 严格物理隔离" --> M4
    
    E2 -. "13. 动态调度 Decode 步" .-> E3

6.2 微观交互时序图 (Sequence Diagram)

该图以时间轴为维度,详细拆解了单次请求在系统内部的流转过程,重点展示了 Context Manager 如何根据 Session ID 进行显存分配与上下文隔离。

sequenceDiagram
    autonumber
    participant Client as 客户端 (Tenant)
    participant Gateway as API 网关
    participant Scheduler as 路由调度器
    participant Engine as 推理引擎 (vLLM)
    participant Cache as KV Cache 管理器
    participant GPU as GPU 物理显存

    Client->>Gateway: POST /v1/chat (Bearer Token)
    activate Gateway
    Gateway->>Gateway: 鉴权 & 提取 Tenant ID
    Gateway->>Gateway: 生成 Session ID (e.g., sess_123)
    Gateway->>Gateway: RPM/TPM 令牌桶限流检查
    
    Gateway->>Scheduler: 转发请求 (携带 Session ID, Tenant ID)
    deactivate Gateway
    
    activate Scheduler
    Scheduler->>Scheduler: 采集 GPU 节点状态 (显存/队列)
    Scheduler->>Scheduler: 执行多维加权调度算法
    Scheduler->>Engine: 路由请求至最优节点
    deactivate Scheduler
    
    activate Engine
    Engine->>Cache: 注册 Session ID, 请求 Prefill
    activate Cache
    
    Cache->>Cache: 检查 Prefix Caching (Radix Tree)
    alt 命中前缀缓存 (如共享 System Prompt)
        Cache->>Cache: 复用已有 KV Cache Block
    else 未命中 (全新会话)
        Cache->>GPU: 申请新的物理显存 Block
        GPU-->>Cache: 返回物理 Block 指针
        Cache->>Cache: 创建 Block Table 映射 (Session ID -> Block)
    end
    
    Cache-->>Engine: 返回上下文状态 (Context State)
    deactivate Cache
    
    Engine->>Engine: 执行 Prefill & Decode 计算
    Engine-->>Client: 返回 SSE 流式响应 (携带 X-Session-ID)
    deactivate Engine

6.3 架构图核心技术点解析

结合上述两张架构图,以下是实现“会话ID与上下文隔离”的几个核心技术机制:

1. Session ID 的贯穿与锚定作用

在架构中,Session ID 不仅仅是一个追踪标识,它是显存管理的唯一主键

  • 在网关层生成后,它被注入到 HTTP Header(如 X-Session-ID)中。
  • 推理引擎的 Context Manager 接收到请求后,以 Session ID 为 Key,在内部的 Radix Tree(基数树)中查找是否存在可复用的 Prefix Cache。
  • 在 Continuous Batching 调度中,调度器通过 Session ID 精准追踪每个请求的生命周期,决定何时将其踢出 Batch 并释放显存。

2. PagedAttention 与 Block Table (逻辑与物理隔离)

这是模型内部上下文隔离的核心(对应架构图中的第 10-12 步):

  • **逻辑隔离 (Block Table)**:Context Manager 为每个 Session ID 维护一个独立的 Block Table(类似操作系统的页表)。逻辑上,每个租户的上下文是连续且独立的。
  • 物理隔离 (PagedAttention):在 GPU 物理显存中,KV Cache 被切分为固定大小的 Block(如 16 个 Token 一块)。Block Table 将逻辑 Block 映射到非连续的物理 Block 上。
  • 优势:这种机制彻底消除了显存碎片。即使租户 A 和租户 B 的上下文长度差异巨大,系统也能通过动态分配物理 Block 实现完美的显存隔离,避免 OOM(显存溢出)。

3. Prefix Caching (前缀缓存) 的跨租户复用

在架构图的 E4 节点,引入了基于 Radix Tree 的前缀缓存机制。

  • 如果租户 A 和租户 B 使用了相同的 System Prompt(例如相同的角色设定或安全护栏),引擎会通过 Session ID 的哈希或 Prompt 前缀匹配,在物理显存中共享这部分 KV Cache Block
  • 这在不破坏上下文逻辑隔离的前提下,极大节省了显存占用,提升了多租户场景下的整体吞吐量。

第六部分:Golang实现完整多租户调用案例

理论需要工程来落地。下面我们将使用 Golang 实现一套完整的大模型多租户调用网关。

6.1 系统架构设计

本案例包含以下核心模块:

  1. HTTP Server:接收客户端的OpenAI兼容格式请求。
  2. Auth & Session Middleware:租户鉴权,提取TenantID,生成SessionID。
  3. Rate Limiter:基于内存的租户级TPM/RPM限流。
  4. Load Balancer / Scheduler:基于后端GPU节点状态的智能路由。
  5. SSE Proxy:处理流式响应的转发、心跳维持和超时控制。

6.2 核心代码实现

1. 项目依赖与结构

1
2
3
4
go mod init llm-gateway
go get github.com/gin-gonic/gin
go get github.com/google/uuid
go get github.com/redis/go-redis/v9 # 实际生产中建议用Redis,此处为演示使用内存

2. 核心数据结构与配置 (config.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "sync"

// Tenant 租户配置
type Tenant struct {
ID string
Name string
Priority int // 优先级 1-10,越大越高
RPM int // 每分钟请求数限制
TPM int // 每分钟Token数限制
}

// BackendNode 后端推理节点
type BackendNode struct {
URL string
MemUtil float64 // 显存利用率 0.0 - 1.0
QueueLen int // 队列长度
MaxCapacity int // 最大容量
mu sync.RWMutex
}

// GatewayConfig 网关配置
type GatewayConfig struct {
Tenants map[string]*Tenant
Nodes []*BackendNode
}

3. 会话ID生成与租户鉴权中间件 (middleware.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package main

import (
"net/http"
"strings"
"time"

"github.com/gin-gonic/gin"
"github.com/google/uuid"
)

const (
ContextKeyTenantID = "tenant_id"
ContextKeySessionID = "session_id"
ContextKeyPriority = "priority"
)

// AuthMiddleware 租户鉴权与会话ID生成
func AuthMiddleware(tenants map[string]*Tenant) gin.HandlerFunc {
return func(c *gin.Context) {
// 实际生产中应从JWT或Header中解析TenantID
authHeader := c.GetHeader("Authorization")
if !strings.HasPrefix(authHeader, "Bearer ") {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing or invalid token"})
c.Abort()
return
}

tenantID := strings.TrimPrefix(authHeader, "Bearer ")
tenant, exists := tenants[tenantID]
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid tenant"})
c.Abort()
return
}

// 生成 Session ID (使用 UUID v4,生产环境可换为 Snowflake)
sessionID := "sess_" + uuid.New().String()

// 注入上下文
c.Set(ContextKeyTenantID, tenant.ID)
c.Set(ContextKeySessionID, sessionID)
c.Set(ContextKeyPriority, tenant.Priority)

// 在响应头中返回 Session ID,方便客户端追踪
c.Header("X-Session-ID", sessionID)

c.Next()
}
}

4. 多租户限流器 (ratelimit.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package main

import (
"net/http"
"sync"
"time"

"github.com/gin-gonic/gin"
)

// TokenBucket 简单的令牌桶限流器
type TokenBucket struct {
rate float64 // 令牌生成速率 (个/秒)
capacity float64 // 桶容量
tokens float64
lastTime time.Time
mu sync.Mutex
}

func NewTokenBucket(rate float64, capacity float64) *TokenBucket {
return &TokenBucket{
rate: rate,
capacity: capacity,
tokens: capacity,
lastTime: time.Now(),
}
}

func (tb *TokenBucket) Allow(n float64) bool {
tb.mu.Lock()
defer tb.mu.Unlock()

now := time.Now()
elapsed := now.Sub(tb.lastTime).Seconds()
tb.tokens += elapsed * tb.rate
if tb.tokens > tb.capacity {
tb.tokens = tb.capacity
}
tb.lastTime = now

if tb.tokens >= n {
tb.tokens -= n
return true
}
return false
}

// RateLimitMiddleware 租户级限流中间件
func RateLimitMiddleware(tenants map[string]*Tenant) gin.HandlerFunc {
// 为每个租户初始化 RPM 和 TPM 的令牌桶
rpmBuckets := make(map[string]*TokenBucket)
tpmBuckets := make(map[string]*TokenBucket)
var mu sync.RWMutex

for id, t := range tenants {
// RPM 转换为 每秒请求数
rpmBuckets[id] = NewTokenBucket(float64(t.RPM)/60.0, float64(t.RPM))
// TPM 转换为 每秒Token数 (假设平均每个请求预估消耗 1000 tokens,这里简化为直接限制TPM)
tpmBuckets[id] = NewTokenBucket(float64(t.TPM)/60.0, float64(t.TPM))
}

return func(c *gin.Context) {
tenantID := c.GetString(ContextKeyTenantID)

mu.RLock()
rpmBucket := rpmBuckets[tenantID]
tpmBucket := tpmBuckets[tenantID]
mu.RUnlock()

// 1. 检查 RPM (每次请求消耗 1 个令牌)
if !rpmBucket.Allow(1) {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "RPM limit exceeded"})
c.Abort()
return
}

// 2. 检查 TPM (预扣减预估Token数,实际生产中需在流式结束时多退少补)
// 这里简化处理,假设每个请求预扣减 500 tokens
if !tpmBucket.Allow(500) {
c.JSON(http.StatusTooManyRequests, gin.H{"error": "TPM limit exceeded"})
c.Abort()
return
}

c.Next()
}
}

5. 智能路由与负载均衡器 (scheduler.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package main

import (
"math"
"math/rand"
"sort"
"sync"
"time"
)

// Scheduler 智能调度器
type Scheduler struct {
nodes []*BackendNode
mu sync.RWMutex
}

func NewScheduler(nodes []*BackendNode) *Scheduler {
return &Scheduler{nodes: nodes}
}

// UpdateNodeStatus 更新节点状态 (实际生产中由节点定时上报)
func (s *Scheduler) UpdateNodeStatus(url string, memUtil float64, queueLen int) {
s.mu.Lock()
defer s.mu.Unlock()
for _, node := range s.nodes {
if node.URL == url {
node.mu.Lock()
node.MemUtil = memUtil
node.QueueLen = queueLen
node.mu.Unlock()
break
}
}
}

// SelectNode 基于多维状态的加权调度算法选择节点
func (s *Scheduler) SelectNode(priority int) *BackendNode {
s.mu.RLock()
defer s.mu.RUnlock()

if len(s.nodes) == 0 {
return nil
}

type NodeScore struct {
Node *BackendNode
Score float64
}

var scores []NodeScore
// 权重配置
w1, w2, w3 := 0.5, 0.3, 0.2
memThreshold := 0.90 // 显存安全阈值

for _, node := range s.nodes {
node.mu.RLock()

// 计算显存惩罚项
memPenalty := 0.0
if node.MemUtil > memThreshold {
memPenalty = (node.MemUtil - memThreshold) * 10.0 // 超过阈值严厉惩罚
}

// 计算队列惩罚项
queuePenalty := float64(node.QueueLen) / float64(node.MaxCapacity)

// 计算计算单元利用率 (此处简化为随机模拟,实际应采集SM Util)
gpuUtil := rand.Float64()

// 综合得分 (越低越好)
score := w1*memPenalty + w2*queuePenalty + w3*gpuUtil

// 优先级加权:高优先级租户得分减去一个 bonus,使其更容易被选中
// priority 范围 1-10,转换为 0.0 - 1.0 的 bonus
bonus := float64(priority-1) / 9.0 * 0.5
score -= bonus

scores = append(scores, NodeScore{Node: node, Score: score})
node.mu.RUnlock()
}

// 按得分升序排序
sort.Slice(scores, func(i, j int) bool {
return scores[i].Score < scores[j].Score
})

// 引入一定的随机性(Exploration),避免所有请求都打到同一个“最优”节点导致热点
// 80% 概率选最优,20% 概率从 Top 3 中随机选
if rand.Float64() < 0.8 || len(scores) == 1 {
return scores[0].Node
}

topN := 3
if len(scores) < topN {
topN = len(scores)
}
return scores[rand.Intn(topN)].Node
}

6. SSE流式代理转发 (proxy.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
package main

import (
"bufio"
"bytes"
"fmt"
"io"
"net/http"
"time"

"github.com/gin-gonic/gin"
)

// ProxySSE 处理 OpenAI 兼容的 SSE 流式转发
func ProxySSE(c *gin.Context, targetNode *BackendNode, reqBody []byte) {
sessionID := c.GetString(ContextKeySessionID)
tenantID := c.GetString(ContextKeyTenantID)

// 构建发往后端推理节点的请求
req, err := http.NewRequest("POST", targetNode.URL+"/v1/chat/completions", bytes.NewReader(reqBody))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create backend request"})
return
}

// 传递必要的 Header
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Session-ID", sessionID)
req.Header.Set("X-Tenant-ID", tenantID)

// 发起请求
client := &http.Client{
Timeout: 0, // SSE 流式请求不能设置整体超时
Transport: &http.Transport{
DisableKeepAlives: false,
},
}

resp, err := client.Do(req)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": "backend request failed", "detail": err.Error()})
return
}
defer resp.Body.Close()

// 如果后端返回非 200,直接透传错误
if resp.StatusCode != http.StatusOK {
c.Status(resp.StatusCode)
io.Copy(c.Writer, resp.Body)
return
}

// 设置 SSE 响应头
c.Writer.Header().Set("Content-Type", "text/event-stream")
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Header().Set("Connection", "keep-alive")
c.Writer.Header().Set("X-Session-ID", sessionID)
c.Writer.Flush()

// 读取后端 SSE 流并转发给客户端
reader := bufio.NewReader(resp.Body)

// 心跳与超时控制
ticker := time.NewTicker(15 * time.Second)
defer ticker.Stop()

done := make(chan struct{})

go func() {
defer close(done)
for {
// 设置读取超时,防止后端假死
resp.Body.(interface{ SetReadDeadline(time.Time) error }).SetReadDeadline(time.Now().Add(60 * time.Second))

line, err := reader.ReadBytes('\n')
if err != nil {
if err == io.EOF {
return
}
// 记录错误日志
fmt.Printf("[%s] Read error: %v\n", sessionID, err)
return
}

// 转发给客户端
c.Writer.Write(line)
c.Writer.Flush()

// 如果是结束标记,退出
if bytes.Contains(line, []byte("[DONE]")) {
return
}
}
}()

// 监听客户端断开连接
ctx := c.Request.Context()
select {
case <-ctx.Done():
fmt.Printf("[%s] Client disconnected\n", sessionID)
case <-done:
fmt.Printf("[%s] Stream completed\n", sessionID)
case <-ticker.C:
// 发送心跳注释,保持连接
c.Writer.Write([]byte(": heartbeat\n\n"))
c.Writer.Flush()
}
}

7. 主入口与路由注册 (main.go)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package main

import (
"fmt"
"io"
"log"
"net/http"
"time"

"github.com/gin-gonic/gin"
)

func main() {
// 1. 初始化配置
tenants := map[string]*Tenant{
"tenant_vip_01": {ID: "tenant_vip_01", Name: "VIP Enterprise A", Priority: 10, RPM: 100, TPM: 100000},
"tenant_std_02": {ID: "tenant_std_02", Name: "Standard Dev B", Priority: 5, RPM: 50, TPM: 50000},
}

nodes := []*BackendNode{
{URL: "http://gpu-node-1:8000", MaxCapacity: 100},
{URL: "http://gpu-node-2:8000", MaxCapacity: 100},
}

scheduler := NewScheduler(nodes)

// 模拟节点状态上报协程
go func() {
for {
// 模拟 GPU 节点 1 负载较高
scheduler.UpdateNodeStatus("http://gpu-node-1:8000", 0.85, 40)
// 模拟 GPU 节点 2 负载较低
scheduler.UpdateNodeStatus("http://gpu-node-2:8000", 0.40, 10)
time.Sleep(5 * time.Second)
}
}()

// 2. 初始化 Gin 路由
r := gin.Default()

// 注册中间件
r.Use(AuthMiddleware(tenants))
r.Use(RateLimitMiddleware(tenants))

// 3. 注册路由
r.POST("/v1/chat/completions", func(c *gin.Context) {
tenantID := c.GetString(ContextKeyTenantID)
sessionID := c.GetString(ContextKeySessionID)
priority := c.GetInt(ContextKeyPriority)

log.Printf("[%s] Tenant: %s, Priority: %d, Request received", sessionID, tenantID, priority)

// 读取请求体
reqBody, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
return
}

// 智能路由选择节点
targetNode := scheduler.SelectNode(priority)
if targetNode == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "no available backend nodes"})
return
}

log.Printf("[%s] Routed to node: %s", sessionID, targetNode.URL)

// 判断是否为流式请求
// 实际生产中应解析 JSON 中的 "stream": true
isStream := true // 此处简化为默认流式

if isStream {
ProxySSE(c, targetNode, reqBody)
} else {
// 非流式请求转发逻辑 (省略,类似 ProxySSE 但不处理 SSE)
c.JSON(http.StatusOK, gin.H{"message": "non-stream proxy not implemented in this demo"})
}
})

// 4. 启动服务
port := ":8080"
log.Printf("LLM Gateway starting on port %s", port)
if err := r.Run(port); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}

6.3 案例运行与测试说明

  1. 启动网关:运行 go run main.go config.go middleware.go ratelimit.go scheduler.go proxy.go
  2. 模拟请求:使用 curl 发送请求。
    1
    2
    3
    4
    curl -X POST http://localhost:8080/v1/chat/completions \
    -H "Authorization: Bearer tenant_vip_01" \
    -H "Content-Type: application/json" \
    -d '{"model": "deepseek-v3", "messages": [{"role": "user", "content": "Hello"}], "stream": true}'
  3. 观察行为
    • 响应头中会包含 X-Session-ID
    • 日志中会打印出该 Session 被路由到了哪个 GPU 节点(由于节点2负载低,VIP请求大概率被路由到节点2)。
    • 如果频繁发送请求触发 RPM/TPM 限制,网关会返回 429 Too Many Requests

第七部分:总结与展望

大模型多租户推理系统是一个融合了分布式系统、计算机体系结构、深度学习算法的复杂工程。本文从会话ID管理算力分配算法两个核心维度进行了深度剖析。

通过对比 DeepSeek 的 MoE/MLA 架构优化,以及 OpenAI、阿里云、腾讯云、百度等厂商的实践,我们可以看到:

  • 没有一种万能的架构,只有针对特定业务场景的最优解。
  • 算力分配的核心在于打破显存墙,通过 Continuous Batching、PagedAttention 以及 Prefill/Decode 解耦,将 GPU 的算力榨干。
  • 多租户管理的核心在于平衡,通过精细化的限流、智能路由和优先级抢占,在保障核心 SLA 的同时,最大化资源利用率。

未来展望

展望未来,大模型推理基础设施将向以下几个方向演进:

  1. 算力网络与端云协同:随着边缘计算的发展,未来的推理将不再局限于云端数据中心。大模型的 KV Cache 和计算任务将在云、边、端之间动态流转,形成泛在的算力网络。
  2. KV Cache 共享与分布式存储:随着上下文窗口向百万 Token 迈进,单机显存将无法容纳 KV Cache。基于 RDMA 和 CXL 技术的分布式 KV Cache 池将成为标配,实现跨节点、跨租户的 Cache 共享。
  3. 推测解码(Speculative Decoding)的普及:通过小模型草稿生成和大模型验证,进一步打破 Decode 阶段的访存瓶颈,将推理速度提升数倍。
  4. Serverless 与按 Token 计费的极致化:云厂商将提供更细粒度的资源切分,实现真正的“用多少算力付多少钱”,彻底消除用户的闲置成本焦虑。

大模型多租户推理架构原理深度解析:会话ID管理、算力分配算法与Golang工程实践(二)

https://www.wdft.com/c854fb8d.html

Author

Jaco Liu

Posted on

2026-09-19

Updated on

2026-09-19

Licensed under