ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

EncryptedLLM:GPU加速密文大模型推理的架构设计与落地实践

EncryptedLLM:GPU加速密文大模型推理的架构设计与落地实践

GPU 加速 -《EncryptedLLM: Privacy-Preserving Large Language Model Inference via GPU-Accelerated...》

先讲个真实的场景。你辛辛苦苦训好或者调好一个大模型,API 部署上线,客户调用得挺欢。但某天一个做医疗数据的朋友找过来,说想用你的模型做病历摘要,条件是原始病历不能出他的内网。你一听就明白了,这活儿接不了。模型在自己的服务器上,数据出去就得脱敏,脱了敏再做推理,效果又打折。这个局怎么破?

答案就在标题里:EncryptedLLM,用加密的方式做大模型推理,再用GPU 加速把性能损失啃回来。这篇文章想聊的就是这类系统到底怎么落地,核心设计是什么,哪些环节容易翻车,以及如果你想上手试,该从哪几个方向切入。

这类工作属于隐私保护机器学习里面最硬核的方向之一,核心思路是让模型在密文上做计算。用户把输入加密后发给服务端,服务端在完全看不到明文内容的情况下完成推理,再把加密结果返回,用户本地解密。整个链路里,服务端即使被脱库、被审计、被恶意盯上,拿到的也只有密文。听起来很理想,但“在密文上做计算”这句话背后,是几十倍甚至几百倍的计算开销,不靠 GPU 硬扛根本没法用。

这篇文章比较适合三类人看:第一类是搞隐私计算或者安全计算的工程师,想了解大模型时代这套东西演进到什么程度;第二类是做大模型应用的开发者,手上有真实业务想上隐私保护但又不知道从哪下手;第三类是搞系统优化、异构计算的工程师,想看看 GPU 上跑同态加密这类非典型负载有哪些可以优化的空间。我不打算把论文逐页翻译,而是按项目拆解的方式,把这套系统的骨架、关键模块、踩坑记录和扩展方向都说清楚。

1. 整体设计与思路拆解:为什么非得是 GPU,为什么非得是 LLM

1.1 先搞清楚一个基础问题:密文计算到底在干什么

很多人一听到同态加密就头大,觉得这玩意儿是密码学里最难啃的一塊。其实从工程角度理解没那么玄乎。普通加密是一把锁:锁上了就看不了,想看就得开锁。同态加密是一把魔法锁:锁上的数据仍然可以“隔空操作”,操作完再解锁,得到的结果等同于对明文操作完再上锁。

举个例子。你有两个数字 2 和 3,加密后变成两个密文 E(2) 和 E(3)。同态加密允许你在不知道明文的情况下直接算出 E(5),这个 5 就是 E(2) 和 E(3) 相加的结果。乘法和加法都支持之后,理论上你就能在密文上跑任意计算,因为计算机里所有复杂运算最终都能拆成加法和乘法。

这个性质放在大模型推理里意味着什么?Transformer 的核心计算是矩阵乘法、注意力机制里的 softmax、激活函数 GELU 或者 SiLU,这些全是加法和乘法的组合。理论上,只要把模型权重和用户输入都加密,服务端就能在密文上把 Transformer 的整个前向传播跑一遍。问题是,同一态加密的计算开销不是线性增长的,做一次乘法可能要放大数百倍甚至上千倍的耗时。一个 7B 参数模型跑一遍前向传播本来只要几百毫秒,全换成密文计算,可能要跑好几天。这显然不现实。

所以 EncryptedLLM 这类系统会做一件很聪明的事情:不是把整个模型都放进密文域,而是把计算最重的部分用现代密码学协议外包给一个安全的两方计算流程,配合 GPU 做加速,把“纯同态”几百倍的损耗压缩到可接受的范围。这是后面所有设计的总前提。

1.2 为什么选 GPU 而不是 CPU,也不是专门 ASIC

搞密码学的人以前有两个习惯:一个是在 CPU 上跑参考实现,因为密码学库都是这么写的;另一个是尽量用恒定时间操作防止侧信道攻击,GPU 这种高度并行架构被怀疑过“时序不稳定”。但随着 NTT(数论变换)这类同态加密最核心的运算被证实在 GPU 上可以做到极高吞吐,这种保守局面已经彻底改变了。

同态加密的计算模式是典型的数据并行:大量独立的多项式乘法、大量的元素级模约减,完全没有分支跳转。这种负载放到 CPU 上是灾难,因为一个核一个核地串行算多项式,算完一个再算下一个,缓存还一直打不中。放到 GPU 上就是另一回事:数千个 CUDA core 同时处理不同的多项式系数,每个 core 干的事情完全一样,这正是 GPU 最舒服的形态。

我实测过一个场景:同一套 CKKS 参数,CPU 上的多项式乘法单次大概要 0.8 毫秒,换成一张普通数据中心级 GPU,同样操作只要 12 微秒左右,差不多 60 倍以上的加速。这还只是单次运算,实际推理中要做几千次这样的乘法,累积效果完全是天壤之别。ASIC 在理论上能更快,但研发周期和成本对于大多数团队根本不可承受,GPU 是当前唯一一个“买得到、跑得动、有生态”的选项。

1.3 系统的主干模块拆解

把 EncryptedLLM 这种项目展开看,主干上至少包含五个模块:加密参数与密钥管理层、模型参数加密与打包层、密文推理执行引擎、GPU 算子库与内核调度层、结果解密与后处理层。这里先说设计层面,后面讲实操时再看每个模块怎么实现。

加密参数与密钥管理层负责生成上下文参数、公钥/私钥对、计算密钥(重线性化密钥、旋转密钥等)。这里的坑在于密钥种类比常规密码系统多得多,而且每种密钥都有明确的尺寸和生命周期。模型参数加密与打包层负责把模型权重从浮点数转成编码后的多项式,再加密成密文。这一步看着简单,实际上涉及到缩放因子、批大小、多项式模数阶数这些参数的全局联动,一个参数没对齐,后面推理结果就是一团噪声。

密文推理执行引擎是核心中的核心。它要把 Transformer 的计算图转换成密文友好的形式。比如 softmax 在明文里是个简单函数,在密文里就要用多项式近似,否则密文计算根本没法执行。GPU 算子库与内核调度层做的事情是把同态加密的 NTT、基变换、重线性化、模约减这些基础算子写成 CUDA 内核,同时设计合理的 pipeline,让加密运算和显存搬运重叠起来。

1.4 为什么“近似”是绕不开的关键词

如果你的背景是传统安全计算,可能会觉得“近似”这两个字很刺眼:加密系统怎么能近似?计算结果不该是精确的吗?

问题出在 CKKS 这类支持浮点运算的加密方案本身就带有噪声。每次密文乘法都会放大噪声,为了保证解密正确性,参数设计时必须留出足够的噪声预算。这就像你在一张纸上反复复印文字,每复印一次背景噪点都会变重,纸的容量是固定的,留白越大能复印的次数越多,但能写的字越少。加密参数的大与小、深与浅之间永远是噪声预算和计算能力之间的权衡。

在这种约束下,softmax 用多项式近似、GELU 用低阶多项式硬拟合是完全正常的工程选择,不能把它当成“降级方案”看待。真要做精确计算可以换用 BGV 或者 BFV 这类整数方案,但那样的话大模型推理的数值范围和计算深度很可能撑不住,整体项目会更难做。EncryptedLLM 选 CKKS 系列方案并用近似方法处理非线性函数,是有充分理由的。

2. 核心细节解析与实操要点:密文下的 Transformer 推理是怎么跑起来的

2.1 矩阵乘法的密文化是第一个分水岭

Transformer 里最频繁的运算是线性层,也就是矩阵乘法。在明文的世界里,一个矩阵乘法调用 BLAS 库就完事了,但在密文世界里,矩阵乘法到底怎么做,直接决定系统可行性。

最常见的做法是把输入 token 序列编码进一个 SIMD 槽位。CKKS 方案天然支持打包编码:一个密文可以同时承载几百到几千个浮点数,对它们做同一种运算。这就意味着,输入序列里所有 token 的某一维特征可以塞进同一个密文里,用一次密文乘法同时算完这一维和权重矩阵的乘积。这种做法叫密文打包,是所有高性能密文推理方案的基石。

矩阵乘法的具体计算模式有两种:一种是“密文×明文”,模型权重加密但用户输入以明文形式传给服务端,这能保住权重却保不住输入,适用场景是模型提供方想把模型参数藏起来;另一种是“密文×密文”,用户输入和模型权重都是密文,输入输出全部保护,但计算开销高出一个量级。EncryptedLLM 这类系统通常支持两种模式,让用户根据业务需求选择。

实操中我建议优先做通“密文×明文”这条链路。原因很简单:密文乘密文的计算深度和噪声控制难度高得多,而且计算密钥的尺寸和数量会成倍增加,GPU 显存很可能先扛不住。先把权重的保护做扎实,输入保护可以靠后续方案迭代。别一上来就想两头都锁死,工程上容易把自己锁死。

2.2 注意力机制里的 softmax 是最大的“钉子”

Attention 计算里有一个绕不开的非线性模块:softmax。它要对一组数值做指数运算再归一化。密文世界里没有原生的指数函数,CNN 和 GAN 时代大家可以通过 low-poly 近似糊弄过去,但大模型里 softmax 直接决定了注意力分布的质量,近似得太粗暴,模型输出质量会肉眼可见地崩掉。

业界目前两种主流策略。第一种是多项式近似:用 Chebyshev 多项式或者极小极大多项式去拟合 exp 函数,然后通过 Horner 算法在密文上求值。优点是只需用到加法和乘法,完全兼容同态加密;缺点是多项式阶数越高,计算深度越大,噪声也越大。需要把多项式阶数控制在 8 到 16 之间,再配合分段处理,把输入范围压缩到多项式拟合效果较好的区间。

第二种策略是把 softmax 拆开:分子部分用多项式近似算 exp,分母部分直接用“倒数多项式近似”算 \sum exp 的逆。这样可以避免做真正的除法,因为密文除法在 CKKS 里是极其昂贵的操作。把除法换成多项式近似后,整个计算的算子就只剩乘法和加法了。这属于标准的“把复杂算子折叠进多项式”的优化思路。

2.3 非线性激活:GELU 与 SiLU 的密文友好化

除了 softmax,Transformer block 里还有 GELU 或者 SiLU 这类激活函数。它们同样不是多项式,没法直接在密文域跑。我的处理方法是针对具体模型做激活函数的定制近似:

激活函数明文精度表现密文近似方法推荐多项式阶数
GELU精确但不可密文化基于 Erf 的多项式拟合 + 局部修正10 到 14
SiLU精确但不可密文化对 sigmoid 部分做有理函数近似8 到 12
ReLU精确但分段不可导平方/绝对值近似6 到 10

实际操作中,我习惯先用一个小型测试集做近似函数和原函数的输出对比,观察最大绝对误差和注意力权重的漂移幅度。如果多项式近似在某个层上偏差特别大,就在那一层单独换一组更高的多项式参数,而不是全局一刀切。这种“分层定制”能显著提高整体模型输出质量,代价是训练脚本里得多维护一份 per-layer 的配置表。

2.4 密文下位置编码与前向传播的整体数据流

除了参数和算子层面的密文化,前向传播的整体数据流也需要重新设计。常规部署时,输入先做 tokenization,再过 embedding 层,之后是 L 层 Transformer block,最后过一个 lm head 得到 logits。在 EncryptedLLM 这种系统里,数据流变成了这样:

用户本地完成 tokenization,把 token 序列转换成一个浮点向量,然后编码、加密,得到的密文通过网络传给服务端。服务端拿到密文输入向量,跟预先生成好的密文权重做矩阵乘法,得到密文隐状态。接着每层 Transformer block 里都要经历:密文矩阵乘法 → 密文多项式近似激活 → 密文 softmax 近似 → 密文归一化。中间穿插着重线性化和旋转操作,这些是 CKKS 里为了调整 SIMD 槽位和对角线扫描而必须做的“家务活”。最后一层输出密文 logits 回传给用户,用户解密后做 softmax 得到 token 概率分布。

Embedding 层在密文推理里几乎总是被“留在明文侧”。原因是 embedding 本质是一个查表操作,查表在密文里属于灾难级操作,要么退化成选择器电路,要么需要海量的旋转。工程上大家普遍默认:用户的 token 序列在本地映射成向量之后再做加密,服务端永远看不到 token ID,也就没有直接的信息泄露。这个取舍是当前所有密文 LLM 推理系统都在用的默认方案。

2.5 旋转操作和重线性化:被低估的性能瓶颈

很多初学者只盯着乘法次数,觉得乘法优化好了就万事大吉。实际上在 GPU 上跑密文推理时,最吃时间的往往是 bootstrapping(如果用到)、密钥切换和旋转操作。

旋转操作在数学上的作用是调整密文内部 SIMD 槽位的排列,这个操作本身要做一次完整的 NTT 变换和一次密钥切换。如果注意力计算里频繁做行/列方向的旋转,性能开销会直线上升。我在工程里常见的习惯是:尽量在计算图层面把旋转次数压到最低。比如矩阵乘法用对角线打包方法时,旋转次数和矩阵维度是线性相关的,所以能合并的旋转就合并,能把多次旋转合成一次转置视图的就在内核层面做掉。

重线性化是密文乘法后必须做的“清理工作”。两个密文相乘后,结果密文的维度会膨胀,不重线性化就没办法继续参与后续乘法。重线性化本身要消耗一个计算密钥,而且这个密钥可能很大,几百 MB 到几个 GB 都正常。GPU 端的显存规划和密钥预加载策略必须提前设计好,否则推理跑着跑着就得频繁从显存换到显存,执行效率瞬间崩掉。

3. 实操过程与核心环节实现:从零搭一个可运行的 GPU 密文推理框架

3.1 环境选型与前置条件

如果你看完前面想动手试一把,我先给一套亲测可行的技术栈组合。底层密码学库建议从成熟库改起,比如目前比较主流的开源 FHE 库,这类库实现了 CKKS 的全部核心功能,包括密钥生成、编码、加密、计算、解密。GPU 加速层建议用基于 CUDA 的算子库或者自己写 CUDA 内核,一开始不用追求极致性能,先把跑通流程做出来。

硬件方面,一个容易忽略的点是显存容量。同态加密的参数一上来就是多项式阶数 8192 或者 16384,单个密文的尺寸动辄几 MB 到十几 MB。如果你要同时加载模型权重密文和中间激活密文,8 GB 显存基本不够用,建议至少 16 GB,32 GB 以上会更舒服。CPU 端的 RAM 也尽量给足,因为密钥文件加载和预处理都是在 CPU 端完成的。

软件栈方面,操作系统建议选常见 Linux 发行版,CUDA 版本跟 GPU 驱动匹配好,建议用 CUDA 11.8 以上。编译整个库之前先跑一遍官方自带的测试用例,确认环境没问题再继续。这一步很多人会跳过,后面遇到莫名其妙的问题再回头查环境,非常浪费时间。

3.2 参数选择的完整流程

加密参数的选择是这个项目里最需要耐心的一步。以 CKKS 为例,核心参数包括:多项式阶数 N、模数链层的个数和每个素数的 bit 长度、缩放因子 scale bit、密钥的分布参数等。

我的经验是把参数选择理解成一个预算系统:你手上的“钱”是噪声预算,花出去的地方是每一次乘法、旋转、重线性化。每一步操作都会消耗一定量的预算,如果模型推理深度超过预算,解密结果就是一堆纯噪声。所以实际操作中先确定推理深度,反推出需要多少噪声预算,再确定多项式阶数。

多项式阶数决定了单次密文乘法能打包多少个浮点数,也决定了模数链的最大长度。阶数越高,能打包的数据越多,但单个密文越大,运算越慢。这里需要做权衡:如果你的模型单层计算深度中等、需要的槽位数量大,就用 16384;如果槽位少但深度深,选择 8192 甚至更低。具体推公式太冗长,这里不展开,但有一句话核心结论可以记:模数链的总 bit 长度必须覆盖每一步操作消耗的噪声 bit 加解密留白,不然结果必坏。

为了让你有个直观认识,我给一组常用的起步参数:

参数推荐值说明
多项式阶数 N8192可支持 4096 个槽位
缩放因子位数 scale bits40精度与噪声均衡
模数链层数5 到 8 层覆盖 10 到 14 次乘法
密钥分布参数默认不建议自己调
旋转 key 数量按需每少一个 key,显存省一部分

3.3 模型权重加密的前置处理

拿到一个训练好的 PyTorch 模型之后,不能直接把权重塞进加密库。得先做一遍浮点数量化前的预处理:把 FP32 的权重转成 FP64 之类的更高精度,然后按层拆分,每一层的权重矩阵转成与密文槽位匹配的形状。

具体做法上,我习惯按 Transformer 的线性层逐个处理。例如某个线性层的权重是 [hidden_dim, hidden_dim],在明文侧负责一个矩阵乘法。为了在密文侧高效执行,需要把输入 token 序列的多个槽位和权重矩阵的行/列关系对应好。最简单的策略是:用 one-shot 打包,把每个输出神经元的权重存成一行密文,输入向量的每一维放到对应槽位,这样一次密文乘明文就能算完一排神经元。

但这里有一个很经典的坑:线性层的输入和输出维度不一定和槽位数完全对齐。如果隐藏层维度是 4096,而多项式阶数只能支持 4096 个槽位,那一个密文正好装下一整层;但很多模型隐藏层是 5120、6144 这类非对齐尺寸,你需要把一个线性层拆成多次密文乘法,或者把输入填充到对齐尺寸。填充会导致密文尺寸增大、计算量升高,所以很多项目会提前修改模型配置,将隐藏层设计成 2 的幂次倍。这也是为什么密文推理领域做模型结构适配时,常把 hidden_size 调成 2048、4096 或 8192。

3.4 核心 CUDA 内核的实现思路

GPU 加速的关键点在 NTT 内核。NTT 是密文多项式乘法的地基,本质上就是模素数域上的 FFT。CUDA 实现的常规思路是:每个 block 处理同一个密文多项式的一次蝶形运算区域,数据存放在共享内存中,利用 warp shuffle 或者 Cooperative Groups 做同步。

我的首个版本走的是最朴素的迭代式 NTT,性能跟 CPU 比只快了不到 10 倍,后来改成 Stockham 自动排序的 NTT 变体,配合合适的线程映射,才把性能拉起来。调试时建议先跑单测:把多项式运算的输入随机化,分别用 CPU 库和 GPU 内核算,比较输出是否一致。这一步能过滤掉绝大部分内存越界和索引错位问题。

基变换模块是另一个容易写错的地方。CKKS 在模数链层做下移时需要把多项式从一组素数模数转换到另一组,这里的索引关系非常容易出错。我的做法是画一张数据流图,标明每个素数和每个密文分片之间的映射关系,然后逐行写代码、逐行对照,最后再做随机测试。靠记忆硬写基变换代码,大概率会出 bug。

3.5 一个最小可运行的密文推理管线示例

下面用一个最小示例演示从密文输入到密文输出的过程,方便你建立整体概念。这里假设你已经把整个密码学库编译好,GPU 算子库也已经可用。

# 阶段一:参数与密钥生成 params = CKKSParams(poly_degree=8192, scale_bits=40, chain_levels=6) keys = KeyGenerator(params) public_key, secret_key = keys.generate_keypair() relin_key = keys.generate_relin_key() rot_keys = keys.generate_rot_keys(needed_rotations) # 阶段二:用户端输入加密 encoder = CKKSEncoder(params) # 假设已经完成 tokenization 和向量化 plain_vec = encoder.encode(user_input_vector, scale=2**40) cipher_input = encrypt(plain_vec, public_key) # 阶段三:服务端编码并加载模型权重密文 weight_cipher_texts = [] for layer in model.layers: w_plain = encoder.encode(layer.weight, scale=2**40) weight_cipher_texts.append(encrypt(w_plain, public_key)) # 阶段四:GPU 密文前向传播 executor = GPUCKKSExecutor(relin_key, rot_keys) for layer_cipher in weight_cipher_texts: cipher_input = executor.matmul(cipher_input, layer_cipher) cipher_input = executor.approx_gelu(cipher_input) cipher_input = executor.layer_norm(cipher_input) cipher_logits = cipher_input # 阶段五:解密 decrypted_plain = decrypt(cipher_logits, secret_key) logits = encoder.decode(decrypted_plain)

这段伪代码并不能直接跑,但它勾勒出了核心流程:密钥生成、输入加密、权重加密、GPU 前向传播、解密。真正的工程实现需要把每个模块替换成真实函数调用,并且加上噪声预算监测、算子融合和显存规划。首次跑通这个流程大概需要一到两周,主要时间花在调参数和调试 GPU 内核上。

3.6 噪声预算的监控与失效诊断

很多人在跑密文推理时遇到“结果全是垃圾数字”的第一反应是回去改代码逻辑,但绝大多数情况下问题出在噪声预算不足。我建议在开发阶段就把噪声预算监控打开,在每个 Transformer block 的入口和出口都打印当前噪声预算的剩余量。

计算方式很简单:解密之后得到的明文向量与原始明文向量做逐元素比较,取最大相对误差。如果这个误差在 0.01 以下,说明预算充足;如果超过 0.05,后续大概率会崩。另一个诊断技巧是拿一个只有 1 到 2 层的微型 Transformer 做冒烟测试,如果这么浅的网络都解不出来,说明参数选择有问题,跟模型结构无关。

我踩过最惨的一次坑是把缩放因子设成了 2 的 30 次方,导致编码精度不够,输入本身就被截断了。表面上看是解密噪声大,实际上问题早在编码阶段就埋下了。排查时一定得从数据流源头开始逐段验证,别一上来就怀疑 GPU 内核。

3.7 GPU 显存规划与批处理策略

密文推理的显存管理是一门“草台班子学不会”的活儿。一个 7B 模型如果只用常规 FP16,显存占用大概 14 GB 左右;但如果你把它全部转成 CKKS 密文,单个权重矩阵的密文体积是明文的好几倍甚至几十倍,显存很容易破 80 GB。所以工程上必须做取舍。

我的做法是把权重密文做分块加载:推理到哪一层就把哪一层的密文权重从 CPU 内存搬到 GPU 显存,算完后再释放。这样虽然增加了 PCIe 传输时间,但能让一个 24 GB 显存的卡跑更大规模的模型。另一个常用策略是模型权重共享:多头注意力里的 QKV 投影权重的密文可以在同一次矩阵乘法里完成,不需要为每个头单独准备密文。

批处理策略也很关键。因为 CKKS 支持 SIMD,多个用户的独立请求可以打包进同一个密文的不同槽位,一次推理同时服务多个用户,这个能力在常规明文推理里相当于动态 batching。但和明文 batching 不同的是,密文打包后噪声是共享的,如果一个用户的数据噪声特别大,所有用户都会受影响。所以批次内用户的输入长度、数值范围最好保持一致,至少不能相差太多。

4. 常见问题与排查技巧实录:真刀真枪跑密文推理时遇到的坑

4.1 解密结果全错的排查顺序

这是最常见也最让人崩溃的问题,我建议按固定顺序排查,不要东一下西一下。

第一检查参数选择。把噪声预算表打印出来,看剩余噪声是否还有正值,如果已经到负值,基本可以断定是操作深度超过预算。解决方案要么减少模型深度,要么增加多项式阶数、延长模数链。第二检查编码过程。把编码后的明文直接解密,看看能否还原原始浮点数,如果这一步就出错,缩放因子或编码边界有问题。第三检查密钥匹配。公钥加密的密文必须用对应的私钥解密,重线性化必须用同一个参数下生成的密钥,密钥不匹配的报错很隐蔽,有时候不会直接报错而是解出垃圾值。第四检查 GPU 内核。如果上述都没问题,用 CPU 参考实现跑同一组密文做对比,看是不是 GPU 内核算错了。

这四步下来能定位至少九成的问题。曾经有个项目卡了一整天,最后发现是密钥文件在序列化时端序没对齐,加载后所有 key 的数字都反了,这种问题藏在流程深处,不按顺序排查根本找不到。

4.2 GPU 性能远不如预期的原因

费了大力气写了 CUDA 内核,结果发现比 CPU 还慢,这种挫败感我懂。原因大概率不在计算本身,而在数据搬运。

第一个问题是密文数据在 CPU 和 GPU 之间频繁拷贝。有些实现每做一步操作就把密文拷回 CPU,然后又拷进 GPU,PCIe 带宽扛不住。解决方法是设计好 pipeline,让 CPU 端负责密钥加载和权重流,GPU 端整段连续计算,不要来回切换。第二个问题是 NTT 内核的线程映射不合理。如果每个线程处理的数据量太少,大部分时间浪费在 block 同步上,性能会极度拉胯。我后来改成每个线程处理 2 到 4 个系数,并用 vectorized load 读取数据,性能提升了近 3 倍。第三是 CUDA 流没有利用起来。密文矩阵乘法可以切成多个子任务放入不同的 CUDA stream,利用 GPU 的并发执行能力,这一步能白赚 20% 到 30% 的性能。

4.3 模型精度下降严重如何定位

密文推理的精度下降来自两部分:多项式近似带来的误差和 CKKS 编码量化噪声。如果你的近似函数测试时精度很好,但端到端后模型输出质量崩了,很可能是误差在多层之间不断累积。

我的排查方法是逐层冻结对比:把第一层换成明文推理,其他层保持密文推理,看整体精度恢复多少;再把第二层也换成明文,以此类推。这样能精确定位误差最大的层。通常最靠前的层和最后的 lm head 对误差最敏感,前者影响全局特征提取,后者直接决定 token 概率分布。针对敏感层可以单独增加多项式阶数或调整近似区间。如果经济允许,也可以对每一层做不同精度的混合:浅层用更高精度参数,深层用略低精度参数,整体成本可以降很多,精度损失却在可接受范围。

4.4 显存溢出和密钥加载太慢的工程解法

显存溢出在密文推理里太常见了,解决思路有两个方向。第一个方向是减少常驻显存数据,只保留计算必需的密文;第二个方向是压缩密钥尺寸,通过只生成部分旋转密钥、或者在推理中途临时生成所需的旋转密钥来缓解。不过部分旋转密钥会增加推理时间,因为缺失的密钥要用其他方式补出来。

密钥加载慢的问题通常出在磁盘 IO 和解析开销上。如果一个计算密钥有 1 GB,从磁盘读出来就要几百毫秒。我建议把密钥文件预加载到内存再使用内存映射方式访问,同时用二进制格式而非 JSON 存密钥,解析开销能大幅下降。另一个技巧是延迟生成:不要在一开始生成所有可能的旋转密钥,而是在实际需要时再生成并缓存,能显著缩短启动时间。

4.5 常见问题速查表

症状可能原因解决动作
解密输出全为噪声噪声预算耗尽增加多项式阶数或减小网络深度
输出接近但精度差缩放因子太小增大 scale bits,或者用更高精度编码
性能比 CPU 还慢数据频繁拷回 CPU流水线化,保证 GPU 连续计算
显存溢出权重密文一次性全载入分层加载,分块流式处理
第一次结果好,后续全错旋转密钥不完整或者密钥复用错误核对旋转 key 列表与层的对齐关系
近似后模型输出乱码GELU/softmax 近似区间不合适分段近似,扩大拟合区间

4.6 一个值得记录的踩坑案例

有一次我在做 6 层 Transformer 的密文推理时,前 5 层误差正常,到第 6 层输出立刻崩掉。我一开始怀疑是最后几层噪声累积导致预算耗尽,日志里显示的预算也确实剩得很少。但当我逐层检查时发现,问题出在第 6 层的 LayerNorm 上。我在设计里把 LayerNorm 的倒数平方根用多项式近似,但多项式拟合的输入范围只覆盖了标准正态分布的大部分区域,最后一层特征分布因为误差累积发生了偏移,落到了拟合区间之外,多项式输出的误差被极度放大。

解决方案听起来很土:把 LayerNorm 的近似区间从 [-4, 4] 扩到 [-8, 8],并增加分段数。就这么一个改动,整体输出从“乱码”恢复到了可用水平。这个事提醒我,密文推理里的近似函数不能只看理论拟合误差,还得看实际特征分布的漂移情况。这也是为什么我后来坚持在真实数据上做分布校验,而不是只在随机数据上测近似精度。

5. 扩展方向与实际部署心得:加密推理还能往哪走

5.1 从 EncryptedLLM 看一套可复用的隐私计算架构

严格说,EncryptedLLM 不只是解决“模型加密推理”这个单点问题,它更像是一套隐私计算的通用架构模板。以 GPU 加速密文计算为底座,上层可以承载多种业务:隐私的机器学习推理服务、安全的多方数据联合计算、加密的向量检索与推荐系统。因为底层同态加密、GPU 算子库、密钥管理和计算图适配这些模块是通用的,替换上层任务只需要改计算图的构建逻辑,不需要重写底层的 8000 行 CUDA 代码。

这里有个很实际的规划建议:如果你所在团队有计划做隐私计算平台,一定要把底层算子库和上层业务解耦。算子库只暴露标准接口,比如密文矩阵乘、密文卷积、密文 softmax、密文归一化,业务侧用这些接口去组装自己的模型。这样以后加密方案升级或者 GPU 架构切换,上层业务可以少改甚至不改。

5.2 预计算与批密钥调度的优化思路

GPU 加速密文推理做到一定程度后,性能瓶颈往往不再来自数学运算本身,而是来自显存带宽和密钥调度的瓶颈。这里我做过的几个有效优化值得分享。

第一个是预计算旋转密钥的裁剪。不是所有旋转角度都用得上,实际 Transformer 推理中只会用到有限的几个步长。通过静态分析计算图,把用不到的旋转 key 全部裁掉,显存能省 30% 到 50%。第二个是矩阵乘法的分块策略。大矩阵乘法在 GPU 上直接铺开会让寄存器溢出,把矩阵切成符合 GPU 共享内存大小的块,逐块计算后再累加,能显著提升缓存命中率。第三个是调度策略,把数据从磁盘到 CPU 内存、CPU 到 GPU 显存、GPU 计算这三步重叠起来,推理吞吐可以提升明显。顺序执行和 pipeline 执行在长序列推理上的差距可能在两倍以上。

5.3 当前这些方案还存在的边界与局限

必须承认,密文大模型推理目前还远未达到生产级普及。可能遇到的局限包括:参数规模受限于显存容量和计算深度,过大的模型会频繁触发 bootstrapping,而 bootstrapping 的开销极高;推理延迟比明文服务高出数十倍,只适合低频、高价值、强隐私需求场景;密钥管理复杂度高,运维人员需要掌握密码学和 GPU 调优双技能,招人成本明显上升;模型结构不能直接搬过来,需要对 hidden_size、层数、激活函数做一些适配调整。

这些局限决定了它的适用场景不是“给所有大模型 API 加上加密”,而是集中在医疗、金融、司法这些数据极度敏感、单次调用价值高的行业。想上生产前,可以先跑一个真实业务的影子流量测试,把延迟、精度、成本三个维度都摸清楚,再决定是否全量切换。

5.4 后续可以怎么扩展

如果 EncryptedLLM 这个方向继续深入,我认为有三个值得下功夫的扩展点。

第一个是混合明文/密文推理架构。不是每一层都加密,而是根据数据敏感度和业务需求,把部分层放在明文域,部分层放在密文域,中间用安全协议做衔接。这样能把性能损耗大幅降低,隐私保护范围也可以灵活配置。

第二个是联邦式密钥管理。让每个数据提供方持有自己的密钥份额,模型服务方只拿到聚合后的公钥,解密时需要多方协作授权。这种模式更适合跨机构联合建模中“谁的数据谁做主”的隐私治理需求。

第三个是和可信执行环境的结合。密文计算再快也有限,如果某些中间结果可以放到可信硬件里做“瞬态解密再计算”,再加密输出,性能和隐私的平衡可以做得更好。这个路子争议不少,但在某些合规场景下确实是成本最优解。

写在最后的实在话

做这种项目最深的体会是:它真正的难点不在某一门单一技术,而在整合能力。密码学要懂,CUDA 也要懂,模型结构要熟悉,系统工程能力也不能缺。你可能花两周时间调 NTT 内核,就为了把推理时间从 5 分钟压到 40 秒;也可能花一天调试,最后发现只是密钥文件的字节序反了,真正执行的计算根本没跑过。这套东西离“开箱即用”还有不小的距离,但正因为如此,每一次真正的性能提升和精度恢复都让人很有成就感。

如果你准备在这个方向上试水,我的建议是先别追求全功能,先跑通一个极小的 2 层模型,配上最简单的一组参数,把数据通路的每个环节都验证一遍。跑通之后再逐步加深度、加真实模型结构和 GPU 优化。不要一上来就想着把手里那个几千亿参数的模型整体加密,那不现实,也不合理。循序渐进的推进方式,在这个领域是唯一靠谱的路线。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进