简要

在这一系列文章中,我们将循序渐进地阐述如何从零开始,构建一个融合对话助手、代码代理和任务代理的统一大模型应用平台。阅读完毕后,您将对“大模型是什么、如何预训练、如何对齐成为ChatGPT、如何产品化部署、如何做检索增强生成(RAG)与智能Agent、如何实现代码助手,以及如何搭建整个平台并做好上线治理”形成深刻且可实践的整体理解。

立即下载保护重要数据:我的密馆 (ValutLib)

全景路线图:

我们将按模块逐步展开,每个模块都是最终搭建完整平台的一块拼图:

A. 基础篇:大模型到底是什么 – 理解大模型的基本概念,包括Token、概率生成、上下文窗口、幻觉现象和评测方法,为后续深入奠定心智模型。
B. 预训练基座篇:从零到一实现预训练Base Model – 通过一个TinyGPT小实验线,介绍如何准备预训练数据、构建Tokenizer、搭建Transformer模型并训练一个小型GPT模型,理解训练曲线和稳定性挑战,并给出现实中扩展到GPT级模型的路线图和限制。
C. Transformer机理篇:深入理解Transformer – 剖析Transformer的核心组件(Attention机制、Q/K/V、多头注意力、前馈网络、残差连接、LayerNorm、位置编码)以及长上下文的衰减问题,从工程视角理解模型结构。
D. 指令微调与对齐篇:高效微调大模型 – 探讨如何通过SFT(有监督指令微调)提升模型指令遵循能力,以及通过偏好对齐(如RLHF、DPO、ORPO)让模型输出符合人类偏好,形成类似ChatGPT的友好对话能力。
E. 参数高效微调篇:LoRA及进阶 – 介绍LoRA/QLoRA等低成本微调方法,以及如何管理多个LoRA适配模块,以实现一套基础模型在多领域、多任务下的定制和治理(融合、路由、版本管理)。
F. 大模型推理部署篇:高效推理与服务化 – 深入大模型推理流程(prefill与decode)、KV缓存机制,探讨吞吐与延迟权衡、并发调度策略、模型量化技术,及如何将模型封装成稳定的服务、多模型路由等部署要点。
G. 分布式训练篇:大模型训练的工程实践 – 学习在多卡多机环境下训练大模型的方法,包括数据并行、张量并行、流水线并行、ZeRO优化,了解通信瓶颈、检查点保存、实验可复现性与训练过程监控。
H. 显存优化篇:显存占用与OOM问题解析 – 拆解训练和推理时显存的构成,提供排查OOM的方法和显存优化清单(梯度检查点、混合精度、梯度累积、量化、缓存策略等),帮助在有限GPU资源下跑大模型。
I. 蒸馏篇:大模型压缩与传承 – 阐述知识蒸馏的重要性,介绍如何通过Logits蒸馏、指令蒸馏、偏好蒸馏等手段将大型模型的能力迁移给小模型,以及建立蒸馏效果的验收体系。
J. 检索增强生成(RAG)篇:让大模型连接知识库 – 全流程讲解如何将向量检索融入大模型应用:文档切分、向量化、检索、混合检索与重排、答案生成与引用,确保答案与证据一致,并讨论RAG系统的评测方法。
K. LangChain篇:大模型应用的工程化抽象 – 学习使用LangChain等框架,将提示(Prompt)、链(Chain)、工具(Tool)、检索器(Retriever)等进行模块化封装,实现复杂任务的链式调用、结构化输出、可观测性、回归测试和灰度发布。
L. 智能Agent篇:类似Manus的任务代理 – 深入解析智能体的工作模式:如何让模型进行任务规划-执行-反馈-反思循环;如何设计安全可靠的工具使用机制(权限控制、幂等性、沙箱、安全审计回放);探索多智能体协作以及评测体系。
M. 多模态篇:拓展模型输入输出到图像/语音/视频 – 介绍如何将图像、语音、视频接入LLM,包括多模态RAG方案和多模态Agent的工具链扩展,让平台具备处理多种模态的能力。

D. 微调与对齐篇:将Base模型打造成对话助手

1. 有监督指令微调 (SFT):教会模型听懂人话

预训练的大模型虽然掌握了丰富语言模式,但不一定会直接按照人类指令行事。例如,让base模型“请总结以下文章”,它可能继续文章内容而不是给摘要。为此,需要有监督指令微调 (Supervised Fine-Tuning, SFT):用一批“指令-正确回答”对示例,来再训练模型,使其学会在看到指令时输出期望的格式和内容。

数据准备: 我们需要收集一批高质量的Instruction-Response对。这可以是人工编写的问答、命令和适当回应。例如:

  • 用户:请用一句话解释地心引力。
  • 模型应答:地心引力是指地球对物体向下的吸引力,使物体落向地面。

这样成对的数据越多越好,涵盖越广泛的任务越好(问答、写作、翻译、逻辑推理等)。因为我们要让模型泛化到各种用户请求。这些数据通常需要人工构造或过滤。像OpenAI的InstructGPT就是用上万条人工示范数据训练而成 。

如果自己没有资源人工标注,可以采用替代方案:利用现有的指令数据集(如Stanford Alpaca的52k指令数据,它其实是GPT-3生成的,但已被证实有效 )。另一个办法是从人机对话日志中提取(假设有客服系统的历史数据)。总之,SFT数据要求指令清晰、回复优质,否则模型会学到坏习惯。

训练流程: 跟预训练类似,但这次我们的样本是 (指令输入, 回复输出) 对。我们微调模型,让它以指令为输入,输出与参考答案尽可能相同。使用的也是交叉熵损失,让模型的生成概率分布在参考答案Token上取得高概率。因为是有监督学习,训练相对简单,无需复杂算法调度。

需要注意的:通常我们会把指令和答案连接成模型的输入,然后只计算答案部分的损失。例如输入格式可以是:

<|prompt|> 用户的指令文本 <|end|>
<|response|> 优秀的回答文本 <|end|>

模型读完整个序列,但我们只在<|response|>...<|end|>范围计算loss,确保模型专注学习如何生成正确回复,而不乱改用户指令部分 。

微调细节:

  • 学习率通常比预训练低,因为不想偏移太多基础语言能力。典型lr在1e-5到2e-5。
  • 数据量如果不大(几千条),可采用多轮epoch直到模型输出稳定改善,但注意别过拟合(监控验证集)。
  • 可以使用混合精度和Gradient Accumulation来加速和适应显存,因为基座模型可能已经较大如7B参数。

SFT效果: 微调后,模型往往已经表现出初步的指令遵循能力。比如之前base模型对指令茫然,现在会尝试回答。经典案例是Stanford Alpaca:在LLaMA-7B基础上微调52k条指令数据,得到的Alpaca模型已经能在单轮指令跟随上与GPT-3.5类似 。这说明SFT非常关键,它把模型从“通识模型”变成“有用模型”。

让我们以一个开源基座模型为例。假设我们有LLaMA-7B原模型,用一批对话指令数据SFT得到一个Chat-7B模型。对比输出示例:

  • 指令:列出太阳系的行星。
  • 微调前(LLaMA-7B):可能输出:“太阳系包含许多恒星和行星,例如Alpha Centauri,…”(不听指令瞎说)。
  • 微调后(Chat-7B):输出:“太阳系共有八大行星,包括水星、金星、地球、火星、木星、土星、天王星、海王星。”(按照指令列举)。

可以看到微调后模型遵循要求回答了,内容质量也有所提升。这正是SFT的价值。当然,它的局限是:模型可能过于遵循训练格式,对没见过的指令表现不佳,而且仍可能胡编乱造内容。为进一步提升,就要靠偏好对齐。

2. 偏好对齐 (RLHF):让模型迎合人类偏好

偏好对齐的目标是:让模型的回答更符合人类偏好,包括内容有帮助、不胡扯、不冒犯。SFT已经朝这方向走了一步,但还不够,因为:

  • SFT使用的示范有限,无法覆盖所有可能输入;
  • 模型可能学会了回答格式但并未真正内化人类偏好(比如仍会在不确定时乱答,而人类希望它直言不懂)。

为此,研究者引入了从人类反馈强化学习 (RLHF) 技术 。RLHF通过人类对模型输出的偏好来二次微调模型,使其回答逐步向人类偏好优化 。经典的RLHF包括三步 (如下图所示)。

图:以RLHF优化模型策略的三个阶段。从左至右依次为:用人类偏好数据训练奖励模型;然后用该奖励指导对话模型的策略优化(通常用PPO算法) 。

步骤1:准备偏好数据 & 训练奖励模型 – 收集一批数据,让人类对模型输出进行偏好比较。例如给定同一用户提示,让模型(通常用SFT后的模型)生成两份不同回复,然后请人标注哪一个更好 。记录为 (prompt, response_A, response_B, preference) 这样的元组。偏好可以二选一或打分。

用这些数据训练一个奖励模型(Reward Model):输入(prompt, response),输出一个评分,用以预测人类偏好 。训练方法是让RM对人类偏好的那个回答打出更高分(通常采用对比损失,使得得分差最大化)。经过训练,RM基本能学到人的偏好,例如更有礼貌、安全、相关的回答会得高分 。

步骤2:策略优化 (Policy Optimization) – 这一步用强化学习的思想,通过与奖励模型交互不断调整对话模型(我们称它为Policy)。具体做法是:以当前Policy生成输出,然后让奖励模型打分,把这个分作为“奖励信号”,用策略梯度方法调整Policy参数,使高分输出概率提高、低分输出概率降低 。

OpenAI使用的是PPO算法(近端策略优化)来实现这一过程 。PPO的细节较复杂,但可以简单理解为一个循环:生成 -> 计算奖励 -> 基于奖励更新模型。通过很多次迭代,模型就学会了讨好奖励模型,也就是更符合人类偏好了。

结果:经过RLHF微调后,模型就变成对齐模型。例如ChatGPT就是在GPT-3.5基础上经过大量RLHF训练的。人类反馈给了它礼貌拒答能力和安全守则等等。当用户要求违规内容时,ChatGPT会拒绝,因为这样的回答在偏好数据中得分很低,它已经学到了 。而在提供帮助时,ChatGPT会更详细、结构清晰,因为这都是人类偏好偏好的风格 。

挑战:RLHF实现起来对资源要求高,需要大量人工标注和迭代算力。不过对于我们使用开源模型的人来说,有一些替代简化方案,下节将提。

3. 直接偏好优化 (DPO):不用RL也能偏好对齐?

RLHF的PPO实现比较繁琐且不稳定,因此最近研究者提出了直接偏好优化 (Direct Preference Optimization, DPO) 。DPO希望跳过强化学习,直接用偏好数据来微调模型。

DPO的核心思想是:有了偏好比较 (prompt + 两个response的优劣),能否构造一个损失函数,使模型直接偏向优胜回答?答案是可以的。DPO推导出一种对比损失,形如:

其中 是用模型自身算出类似“得分”的东西, 是赢家和输家回答。简单说,让模型的策略得分更倾向优胜回答,类似在做Logistic回归区分好坏答案 。

实现上,DPO需要有一个参考模型(通常就是SFT模型固定不动)来提供基准概率,然后微调模型去提升优胜回答相对于劣回答的对数概率 。这样的优化能直接让模型输出更接近人类偏好而无需训练奖励模型或采样PPO序列。

DPO的优点是简单稳定:它就是普通的监督对比训练,不需要复杂的RL算法调参,梯度更新在标准框架内完成。一些实验表明DPO效果可媲美PPO的RLHF,但训练成本大幅降低。

对于工程应用,如果有偏好标注数据,可以尝试DPO这种方法对齐模型。据报道在低资源(较少偏好数据)情况下,DPO效果甚至优于PPO 。

4. 无参考整体偏好优化 (ORPO):再简化一步?

更进一步,有研究提出ORPO (Odds Ratio Preference Optimization) 。它设想不需要参考模型,直接在SFT微调时融入偏好信息。ORPO的方法是:在常规交叉熵损失上加一个“赔率比”项,如果一个生成是偏好中被拒绝的类型,就给予轻微惩罚 。这样模型在一次微调过程中同时学习指令任务和偏好倾向 。

ORPO据称能在一个阶段完成对齐,无需分RLHF阶段,节约了流程复杂度 。一些实验对7B模型用ORPO微调,发现效果超过用两阶段RLHF得到的13B模型 。当然这是新兴方法,工业界落地仍需验证。不过它代表一个趋势:尝试用更简单直接的训练目标取代多阶段RLHF,只要能达到相似效果,这对小团队是很有帮助的。

无论PPO、DPO还是ORPO,背后都是利用偏好数据来调整模型行为。偏好数据获取是瓶颈(需要人工),但一些开源项目开始众包或分享偏好数据集,比如OpenAssistant等社区收集了大量用户对话和评价,可用于训练。

5. 对齐微调的工程注意事项

在实践中,对齐模型需要注意:

  • 防止过度惩罚:RLHF若过头,模型可能变得过于保守,遇到稍复杂问题就频繁道歉或拒答,这就是所谓“模型过度顺从”。需要在鼓励安全与保持有用性中平衡。
  • 多样性:人类偏好并非单一标准,比如有时幽默感也是优点。训练数据里最好有多样风格,否则模型会趋向一种刻板语气。
  • 持续迭代:对齐不是一劳永逸。上线后收集的真实用户反馈可以不断加入训练,提升模型契合实际用户偏好的程度。OpenAI就持续在收集ChatGPT反馈改进模型。
  • 评估:对齐成功与否要通过专门评测,比如红队攻击(尝试诱导出坏内容是否有效阻止)、人工盲评(对比对齐前后的回答好坏)。需要制定明确标准,以便迭代时知道有没有进步。

对个人项目而言,可以从小规模人类评估做起。比如找几位同事朋友,对你微调前后的模型输出打分,看是否总体偏好后者。如果没有条件,可以采用GPT-4作为代理评分,虽然有偏差但聊胜于无。

E. 参数高效微调篇:LoRA与多域适配

1. LoRA:冻结大模型,微调小参数

大模型通常有数亿甚至上千亿参数,直接微调不仅耗时,还容易过拟合小数据。LoRA (Low-Rank Adaptation) 提供了一种巧妙的方案:冻结原模型权重,只向其中注入很少量的新参数,让模型的能力通过这些新参数来调整 。

LoRA的方法是针对某些权重矩阵(通常是Transformer里的W_Q、W_K、W_V或W_O等大矩阵)进行低秩分解。具体而言,对于一个原权重矩阵 ,LoRA引入两个小矩阵 (r远小于d,k),使得微调效果相当于在原输出上加上 。这里 被称为权重更新矩阵,它的秩至多是r,所以叫低秩适应。

训练时,我们固定原权重 W,不更新,只训练 A和B这两个小矩阵 。因为r很小,比如常取8或16,这意味着可训练参数远远少于全模型参数。例如一个6B模型可能只需更新几十M参数,不到1% 。这带来多方面好处:

  • 显存占用降低:只需为小矩阵计算梯度,其余大权重不变 。
  • 计算更快:参数少了,反向传播成本降低。
  • 避免灾难遗忘:原模型知识保留,LoRA只是在局部做细调,训练不容易破坏原有性能。

直觉类比: 把原模型比作一座豪华大楼,LoRA就是搭个脚手架来局部装修,而不动大楼主体结构。这样成本低,而且如果不想要了,拆掉脚手架(移除LoRA模块)大楼还是原样,不会损坏。

LoRA通常应用在Transformer的Attention和FFN层的投影矩阵上,因为这些地方参数多又通用。注入LoRA相当于给这些层多加了一条支路输出。推理时,LoRA模块计算 并加到主分支上,对总计算量略有增加(但可忽略不计,r很小)。也可在推理前将LoRA的AB乘积合并回原权重,这样就完全不增加计算,只是得到一个新权重 = W + ΔW;不过通常我们不这么做以保留切换LoRA的灵活性。

2. QLoRA:用4位量化进一步减负

QLoRA是LoRA的增强版,全称Quantized LoRA。它的出发点是:大型基座模型权重能否低位表示以减小显存占用?答案是可以——通过量化,把模型权重从16-bit压缩到更低位,比如4-bit 。

QLoRA的典型流程:

  • 先将原模型权重加载为4-bit量化形式 。比如采用NF4(第四阶近似浮点)这种量化方案,最大限度减少精度损失。4-bit意味着参数占用降低到1/4。
  • 锁定这些量化权重不变(不可训练),然后同样引入LoRA低秩矩阵进行微调 。因为权重大幅压缩,我们省下的显存可以用来放更大的模型或更大batch。
  • LoRA部分仍用高精度(一般16-bit)进行更新。QLoRA确保即使主模型4-bit,LoRA精度够高,总体效果依然接近全精度微调 。

QLoRA的震撼之处在于:它成功在单张消费级GPU上微调出了65B参数模型(LLaMA-65B),达到与原模型齐平的性能 。这是过去不可想象的,因为65B模型FP16需要占据130GB显存,而QLoRA 4-bit压缩+LoRA微调使得在24GB卡上就搞定了 。

因此,对个人开发者来说,QLoRA打开了在本地fine-tune超大模型的大门。当然,量化也有策略和权衡,比如选择合适的量化方案(NF4被证明比简单INT4效果好),对不同层可采用不同bit宽等。但开源的bitsandbytes库已经实现了QLoRA方便的接口,只需一句model = prepare_model_for_kbit_training(model, use_gradient_checkpointing=True)即可把模型量化为k-bit训练。

3. 多LoRA融合与切换:一模型身兼多能

当我们使用LoRA技术,意味着可以针对不同任务或数据训练出多套LoRA权重,而保持基础模型相同。这带来了一个有趣的可能性:一个大模型 + 多个LoRA模块 = 在不同场景表现出不同能力。

设想你有一个13B的通用模型,通过LoRA微调得到:

  • LoRA_A:医疗问答专家
  • LoRA_B:法律咨询专家
  • LoRA_C:写诗模式

每个LoRA模块参数量很小(几十MB),加载也快。我们可以根据用户请求的场景选择性地加载对应LoRA叠加到模型上,使输出风格切换到那个领域。比如用户进入医疗模式,我们应用LoRA_A,模型就像“加了医疗滤镜”一样,更懂医学术语和安全界限。

LoRA的融合(Fusion): 有时我们想要组合多种能力。如果LoRA模块A和B不冲突,是否能同时应用?实践上可以尝试将多个LoRA权重直接累加到模型(相当于 ΔW总 = ΔW_A + ΔW_B)。这叫LoRA融合。比如想让模型同时懂医疗和法律,就把两套改动都加进去。但要小心,融合效果不一定理想,因为不同LoRA可能在修改相同权重元素时互相干扰。为了改进融合,可以:

  • 先后顺序应用,并给予不同缩放系数(如让某个LoRA影响减半)。
  • 对输出再做一点微调,使融合结果平衡各能力。

有研究提出训练一个“LoRA融合器”,即再训练一个小网络来自适应组合多个LoRA。但简单场景下直接加常常可行,例如给模型同时加载翻译LoRA和总结LoRA,它或许就能翻译并总结。

多LoRA路由: 在平台应用中,我们可以实现自动选择LoRA。比如构建一个分类器模型,先判断用户query属于医疗还是法律,再在后台对LLM加载相应LoRA回答。这使用户体验像使用一个全能模型,但其实底层做了路由。LangChain等框架支持这种RouterChain结构。

LoRA版本管理: 当团队多人协作训练LoRA时,会产生许多版本。需要有治理策略,否则容易混淆。建议:

  • 对每个LoRA模块打标签,包括基座模型版本、数据集、用途。比如 “LLaMA2-13B-med-v1.0” 表示基于LLaMA2-13B训练的医疗LoRA第一版。
  • 保留评测记录:每个LoRA上线前应该在人造评测集上测试其能力及是否破坏基础模型其它任务性能。
  • 灰度发布 LoRA:直接上线某个LoRA有风险,可以小流量测试。例如先让5%用户走新LoRA,观察反馈。

移除LoRA: LoRA的另一个治理优势是易移除。发现某LoRA不好,卸载它模型就回到原始状态,不像全参微调那样想回退不容易。也可以动态启停某LoRA,比如当检测到用户问题涉及代码,就加载“代码辅助LoRA”,否则卸载以减少干扰。

4. 应用案例:以LoRA扩展ChatGPT功能

一个典型案例是:在开源ChatGPT模型基础上,用LoRA训练不同插件功能:

  • 数学LoRA:强化算术和公式输出格式
  • 编程LoRA:大量编程问答数据微调,提高代码正确率
  • 文学LoRA:让回答更具文艺风格

当用户提问数学题时,系统后台先识别是数学类型,于是启用数学LoRA,模型准确输出推导过程。当用户问代码bug,则应用编程LoRA,模型提供正确的代码建议。如果问写诗,就加载文学LoRA,让回复更诗意。

这样通过模块化LoRA,整个系统的可塑性大大提高。新领域需求来了,不必训练新模型,只需用相应数据finetune出LoRA模块挂上即可。而且不同模块之间解耦,互不影响基础模型和别的任务。

值得强调的是,多LoRA方案效果也取决于基础模型的强大程度。基座模型越通用,LoRA适配效果越好。就像白纸上画线条清晰,若底色斑驳再盖色就难调和。因此选择好的预训练模型(如训练于多领域的大模型)很重要,这也是开源社区致力于提供强大基座的意义。

F. 大模型推理部署篇:高效推理与服务化

1. LLM推理两阶段:预填充与自回归生成

理解大模型推理优化,先要明白推理时模型在做什么。以文本生成为例,当我们给模型一个Prompt要求它续写时,推理可分为两个阶段:

  • Prefill 阶段(预填充):模型先处理完用户提供的所有输入Token。这相当于一次普通的前向传播,通过所有Transformer层,得到输入序列每个位置的输出表征。
  • Decode 阶段(自回归解码):接下来模型要逐个生成输出Token。生成每一个Token,都需要将包含新Token的整个序列通过模型,再预测下一个,循环往复。

但是,如果每次生成都从头跑一次Transformer,那大量计算会重复(前面的上下文没变)。KV缓存就是为了解决这个重复计算而出现的 。

2. KV缓存:不重复劳动,加速解码

在Transformer的自注意力机制中,每层都会基于输入计算Key (K)和Value (V)矩阵用于注意力。对于一个长度L的序列,当模型生成下一个Token时,前L个Token的K和V实际上和上一次生成时已经算过的K、V相同。没必要重新算。

KV缓存的原理就是:缓存每层的K、V值,在生成新Token时,只计算新的Token部分,然后将它的K、V附加到缓存中,其余部分直接复用 。这样,生成第N个Token的计算量相较从头推理,减少了前N-1步的K,V运算和大量注意力乘法。

比如,第一步生成token1时,做了长度L0(prompt长度)-> L0+1计算;生成第二个token时,如果无缓存,要做L0+1->L0+2全序列计算,但有缓存,只需算新来的token,从长度1->2增长的那一点 。当生成到第m个token,有缓存的话每步算量近似常数(只算新token相关部分),而无缓存算量随序列变长线性增加。

Manus团队的经验表明,KV缓存可将长上下文的推理成本降低一个数量级以上 。他们甚至量化过:Claude模型使用缓存时每百万Token成本0.3美元,不用缓存是3美元 。可见差别巨大。

工程实现上,常用做法是预分配张量来保存每层的KV,每生成一步往里填充新内容。框架如HuggingFace transformers在generate时会维护一个past_key_values用于存KV缓存。作为开发者,一定要利用这个功能,除非只生成极短文本,否则性能损失不可接受。

3. 吞吐 vs 延迟:批处理与并行

在部署中,我们面临两个关键指标:

  • 单请求延迟:一个用户请求从发出到拿到结果的时间,越低越好。
  • 整体吞吐:系统每秒能处理的Token数量或请求数量,越高越好。

二者常此消彼长。为了增加吞吐,我们可以用批处理(batch):把多个请求的输入合并一起,通过一次模型前向并行计算出来 。现代GPU对并行计算效率更高,所以批量推理摊平了成本,平均每请求算力占用下降,提升总吞吐。

但批处理会使个体等待变长。例如系统凑齐了8个请求一起算,那第一个请求可能在队列里等了几个毫秒凑batch。而如果请求很少,还硬要攒个大batch,就会导致延迟增加。因此需要权衡:一般做动态批量,设置一个最短延迟阈值和最大batch大小,在阈值内尽量攒请求,不到阈值也及时执行。

除了批处理,还有并行解码技术:比如多线程分别跑不同请求,每个线程占用部分GPU,或使用模型并行一批生成。典型的生产服务会结合两者:在每个时刻动态组成batch在GPU上跑,但同时可能有多个batch管道式执行,不让GPU空闲。

流式输出也影响延迟感受:LLM生成长文本通常会边生成边输出给用户(像ChatGPT那样一字字显示)。这不减少总延迟,但改善了交互体验。工程上实现流式通常通过HTTP长连接或WebSocket,不断推送部分Token结果。很多模型server框架(如FastAPI、Sanic)支持流式响应。要注意确保缓存刷新及时,不要因流式发送而频繁重启推理(应保持上下文缓存持续累积)。

调度实例:

假设有16个并发用户请求。我们的服务器设置最大batch=4,等待阈值=5ms:

  • 当请求陆续到来,如果在5ms内积累了4个,就打包成一批提交GPU推理prefill。
  • 如果只等到了2个就超时5ms了,也直接跑这2个以免让它们久等。
  • GPU生成第一个Token给这批后,会将结果拆分给各请求并流式返回,再将这些请求放回队列等待生成下个Token。此时可能又新进来一些请求,与剩下的未完成请求一起混合重新打批,继续下一步生成或prefill新的。

这个调度过程复杂但非常关键。优秀的调度算法可以大幅提高GPU利用率。开源项目如 vLLM 提出了智能调度器(Ordered Serialization)来最大化 batch 利用,同时用异步执行减少各请求等待 。这些细节对大规模服务很重要,但这里不展开。要知道“小批多次”往往比“一次一条”效率高很多,前提是有一定并发量支撑。

4. 模型量化部署:速度与精度的权衡

在推理场景,量化模型是一种常见优化。通过使用8-bit甚至4-bit权重代替16-bit,可以显著减少显存占用,也利用低精度Tensor Core提升推理速度。

8-bit量化(如INT8)通常能在几乎无精度损失情况下,将模型推理速度提高约20-30%,显存减半。许多框架提供INT8推理支持,例如NVIDIA的TensorRT, FasterTransformer等,都能加载Transformer权重做高效8-bit推理。

4-bit量化则更进一步,但精度可能有轻微下降,尤其对于敏感任务。但很多开源13B以下模型用4-bit生成日常文本效果依旧不错。4-bit的优势是可以在更小GPU上加载更大模型,从无到有提供能力。

何时需要量化?

  • 当你的模型接近GPU显存上限时,8-bit可以省下空间让你用更大batch或更长上下文。
  • 当你要部署在CPU或移动设备上,甚至需要用更低bit(如3-bit, 2-bit,甚至通过蒸馏得到小模型)。
  • 如果对输出精度要求极高(比如法律文本逐字不能错),可能宁可上FP16保险。但大多数Chat场景8-bit无碍,4-bit也可用。

有了QLoRA的前车之鉴,4-bit微调模型其实也能用4-bit推理。所以一套流程:从FP16模型量化微调->得到4-bit LoRA模型->部署时合并LoRA到量化模型->直接4-bit推理,即可获得几乎原始性能。像vicuna-13B这样的对话模型就常以4-bit版发布以方便用户部署。

5. 服务封装与多模型路由

将模型推理功能包装成服务,需要考虑:

  • 接口协议:常用REST API或WebSocket协议。REST简单但有响应头开销,每次请求要新连接,不利频繁流式数据。WebSocket则可保持连接,适合持续对话。很多现有LLM服务用HTTP SSE(Server-Sent Events)实现流式,就是HTTP长连接不断推送数据。
  • 多线程/异步:Python里可用异步IO或多线程来等待GPU结果同时处理其他任务。但Python的GIL对CPU绑定线程影响不大,因为推理大头在GPU。可以使用asyncio来让请求协程化,这样易于批量处理。C++服务更高效,但实现复杂,通常只有在极限性能要求下才自研C++服务。
  • 热加载:服务应支持动态加载模型和切换。例如提供API端点来更换当前基础模型或加载新的LoRA模块,这样不用停机维护。为此,框架需要设计模型管理器,可在后台下载权重、初始化模型,然后更新路由配置。
  • 日志与监控:日志记录每个请求使用了哪个模型/LoRA、响应时间多少、截断没有等。监控GPU利用率、显存、延迟分布,这些可以接入Prometheus之类的系统统一处理。
  • 错误处理:要处理推理中可能的异常,如显存溢出(应该捕获并降级,如用更小模型)、超时(若模型在规定时间没出结果,就中断返回部分或提示重试)。另外输出内容需要进行安全过滤(如如果检测到输出不合规,在返回前做遮蔽或替换),这些通常作为后处理hook实现。

多模型路由: 在平台,我们可能同时托管多个模型:不同大小(快慢)、不同专长(如代码模型、对话模型)。实现路由需要:

  • 自动识别:基于输入内容选择模型。例如通过关键字或训练一个分类器。简单例子:如果用户问题含有代码片段,用Code模型,否则用Chat模型。
  • 链路配置:某些任务可能用组合,比如先用任务规划模型拆分,然后对每子任务调用相应专长模型。LangChain能帮忙组织这样的调用链。
  • 负载均衡:若有多个GPU,每个GPU跑一个模型实例,可以按空闲情况分配请求过去。这类似web服务负载均衡,只是需要感知模型繁忙程度。

一个精妙的策略是大小模型兜底:平时使用小模型服务以获得低延迟,但当检测到小模型回答可能不可靠时,再调一个大模型交叉检查或直接覆写答案。这种两阶段服务在一些应用中节省成本的同时保证质量。不过实现复杂,需要可靠的“不确定性估计”机制来让小模型知道自己不行了(可用logits熵或置信度等)。

6. 示例:构建一个对话模型API服务

让我们设计一个简单的对话API:

  • 客户端通过POST /generate 提交 {"prompt": "...", "history": [...]},服务器返回一个stream,内容分块为已生成的片段。
  • 服务器有一个全局的模型实例(比如Chat-13B加载在GPU0)。每个请求进入后,在一个异步队列等待。调度器将积累请求组成batch张量丢给模型的generate函数,设定max_new_tokens、温度等超参数。
  • generate调用过程中,使用stream_output=True获取增量Token。每来一个Token,就通过已打开的HTTP响应流发送出去。
  • 如果多个请求同时生成,调度器可能使用一定算法把生成步骤交织,如先为batch生成1个token,分发,然后看看有无新请求加入,再继续下一个token。
  • 如此直到每个请求都完成预定长度或遇到终止符。然后关闭流,记录日志。

优化点包括:可以维护会话状态,对于同一个会话的连续请求,保留其past_key_values以免每次都重新prefill。这类似支持对话上下文,很多实现会传会话ID来关联缓存。但这也带来内存管理问题,需要为每个会话存缓存且限制最大会话数(否则内存泄漏)。

安全过滤可以在发送前加一步,比如用关键词屏蔽或更高级的内容审核模型扫描。如果违规就中断生成并返回警告消息。

7. 部署硬件与并发

部署大模型最好用带GPU的服务器。CPU虽然可以跑小模型,但慢两个数量级且耗电高,不经济。多GPU可以:

  • 垂直扩展:用模型并行技术跑一个超大模型(如70B在2卡上),但通信麻烦推理增益有限。
  • 水平扩展:每卡跑一个实例,前面用负载均衡,扩容更方便。这是常见方案。

对于并发请求很多的场景,可以启用多进程多实例,每个实例绑定一张GPU或甚至一张GPU启动多个实例(但要小心显存分配冲突和上下文开销)。实践发现,一个模型实例通常可以有效利用GPU直到一定并发度,再开第二个实例收益变小,因为GPU算力主要花在矩阵乘法上,不容易并行更多。但为了隔离和利用CPU多核资源,可以开多个进程,每进程GPU用量控制在一定范围。

例如GPU 40GB,可以起2个20GB权重的模型进程,这样每个服务不同流量(比如一个专门处理聊天,另一个处理代码)。或者起同样模型的多个副本来分担高峰请求。

监控与弹性:要监控每个实例的吞吐和延迟,遇到持续高负载,可考虑弹性扩展(如果有云GPU可以动态申请更多实例)。反之闲置时释放资源。但GPU不像CPU那么易申请,弹性需要一定缓冲时间来加载权重。所以往往是手动扩缩容或者固定集群跑。

资源隔离:一个Agent任务可能调用多个模型(Chat模型、工具模型等),需要确保不同模型各用各的GPU或显存划分,否则互相抢资源会导致不稳定。容器化可以封装不同服务,但显卡直通需要注意驱动版本统一等。

故障恢复:模型server最好有崩溃自动重启机制,比如用supervisor或K8s配置。当权重较大时,也要考虑启动耗时,可以用dump加载后的模型状态到磁盘(通过pickle等)以便快速冷启动。

总而言之,大模型服务化需要多方面优化,才能既快又稳又安全。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐