上个月我把公司几个开源大模型接入内部知识库,同事们用得很开心,但法务和合规部门几乎同时找上门:用户提交的合同、病历、财务流水,就这么明文扔给云端模型,出了问题算谁的?这个问题其实不是个例。大模型能力越强,输入就越敏感,隐私优先不再是一句口号,而是每个落地项目都得正面回应的问题。
我花了大概三周,把“同态加密”这个听起来很学术的东西和大模型推理链路实际结合跑了一遍。这篇文章不打算重复教科书理论,只记录一条已经验证过的可行路径:边界在哪里、哪些能做、哪些目前做不了、参数怎么选、坑在哪里。适合正在做大模型应用、又对数据隐私有要求的开发者或架构师参考。
1. 隐私优先的大模型,到底在保护什么
1.1 三种典型的隐私泄露风险
做大模型应用的人,第一反应通常都是“用户输入的提示词里不能有敏感信息”。这个直觉对,但不完整。一条完整的推理链路里,隐私泄露至少有三个方向,后面做方案时容易漏。
第一个方向是推理输入的泄露。用户把一段病历摘要、一份没有脱敏的合同发给云端模型,模型服务方理论上能看到全部内容。很多内部系统的做法是让用户自己“注意别发敏感内容”,这本质上把责任转嫁给了用户,不可控。
第二个方向是模型权重的泄露。商业公司花钱微调出来的模型,部署在公有云上之后,攻击者可以通过大量构造输入、观察输出,用蒸馏的方式把模型能力“偷”出来。模型本身是公司资产,这个泄露方向经常被忽略。
第三个方向是推理结果与中间状态的泄露。日志系统、监控系统、缓存层里可能残留用户输入和模型输出,甚至包括embedding向量。这些中间态一旦被拖库,敏感信息照样外流。
同态加密(Homomorphic Encryption,简称HE)解决的是前两个方向的强相关问题:把输入和模型权重都变成密文,让计算在密文上进行,服务方只能看到“加密状态下的计算过程”,看不到原始输入和原始权重。听起来很理想,但工程上能否落地是另一回事。
1.2 同态加密与其他隐私计算方案的取舍
我在选技术路线的时候,把市面上能用的隐私计算方案都过了一遍,主要对比了安全多方计算(MPC)、可信执行环境(TEE)、联邦学习和同态加密。
| 方案 | 核心思路 | 通信开销 | 计算开销 | 难点 |
|---|---|---|---|---|
| 安全多方计算 | 多方各自持有分片,联合计算 | 高,多轮交互 | 中等 | 网络延迟敏感 |
| 可信执行环境 | 硬件隔离,密钥不出CPU | 低 | 低 | 依赖硬件,需信任厂商 |
| 联邦学习 | 模型参数共享,数据不出域 | 中 | 低 | 只能用于训练,推理保护弱 |
| 同态加密 | 密文直接参与计算 | 低,一次性交互 | 高,计算量成倍增长 | 性能、噪声、实现复杂度 |
大模型推理场景有一个特点:用户发一次请求,服务端算完直接返回结果,交互轮次最好不超过一次。同态加密天然适合这种“一次交互”的模式,因为客户端把输入加密后发给服务端,服务端在密文上算完返回,客户端用私钥解密即可,中间不需要像MPC那样多轮协商。TEE在性能上确实最优,但需要你信任硬件厂商和云服务商的供应链,有些合规场景并不接受。
所以我的结论是:如果目标是“推理阶段保护输入和权重”,同态加密是方向上最合适的候选,难点在性能和工程实现。下面这些内容全部围绕“怎么把HE用到大模型推理上”展开。
2. 同态加密的大白话原理与库选型
2.1 “能算数的保险箱”:加密状态下的运算
同态加密用一句话概括就是:在不可见内容的前提下,对密文做运算,结果解密出来等于对明文做同样运算的结果。用数学表示就是E(a) + E(b) = E(a + b)或者E(a) * E(b) = E(a * b)。你可以理解成一个保险箱:箱子是透明的但内容是被锁住看不到的,你可以在不打开箱子的情况下往里面塞一个数,或对箱子里的数做加法乘法,最后打开箱子拿到的结果,和你直接对原始数字做运算拿到的结果一致。
这个性质对大模型推理的意义很直接:用户把问题加密后发给模型服务方,服务方在密文上做矩阵乘法、做激活函数计算,模型方全程看不到真实问题内容,最后用户解密拿到的还是正常推理结果。同理,如果模型权重也加密部署,服务方自己都不知道自己跑的是什么样的网络结构。
大模型领域常用的同态加密方案叫CKKS,它的特点是支持浮点数的近似计算,这和机器学习推理的数值表达习惯一致。CKKS在做加密之前会把浮点数编码成多项式,同时乘上一个缩放因子来保留精度,计算结束后再把结果缩放回去。
2.2 四种主流HE方案怎么选
同态加密按能力强弱分成几个层级,选型时容易混淆,先看对比:
| 方案 | 类型 | 支持运算 | 适合场景 |
|---|---|---|---|
| Paillier | 部分同态(PHE) | 仅加法 | 聚合统计、投票、梯度聚合 |
| RSA/ElGamal变体 | 部分同态 | 仅乘法 | 简单聚合,场景有限 |
| BFV/BGV | 全同态(FHE) | 加法+乘法 | 整数精确计算、隐私查询 |
| CKKS | 全同态(FHE) | 加法+乘法 | 浮点近似计算、机器学习推理 |
| TFHE | 全同态(FHE) | 布尔电路运算 | 任意函数,逐bit运算 |
Paillier在很多隐私计算项目里流行,因为它效率高、容易理解,但它只能做加法,无法完成矩阵乘法,大模型推理基本用不上。BFV能做精确整数运算,适合计数和聚合,但浮点权重需要缩放成整数,精度损失控制很麻烦。CKKS是现阶段做机器学习推理的最优选择,代价是结果是近似的,需要在使用时容忍一定的误差。
所以下面实践部分我全部使用CKKS,这是目前大模型落地相对现实的入口。
2.3 动手前必须搞懂的三个参数:scale、模数链、明文槽
第一次看HE代码的人很容易被一堆参数劝退,其实核心只有三个。
第一个是缩放因子scale。CKKS把浮点数编码成整数时需要放大,scale就是放大倍数,通常取2的幂次,比如2的40次方。scale越大,浮点精度越高,但也会更快耗尽后面的噪声预算,所以不是越大越好。
第二个是模数链。CKKS的密文噪声会随着乘法次数增长,为了控制噪声,每做一次乘法之后可以做一次“重缩放”,把密文缩小回原来的规模。模数链就是预留的一串模数,每重缩放一次消耗掉一级。模数链越长,能支持的乘法深度越大,但密文体积也越大。
第三个是明文槽。CKKS支持把多个数打包进同一个密文的多个槽里,用一个密文模拟出一整条向量运算,这就是SIMD(单指令多数据)效果。做大模型推理时,一个矩阵乘法的十几个中间神经元可以塞进同一个密文并行计算,这是目前压缩性能差距的最重要手段,后面的实践会用到。
我把这三个概念记成一句话:scale决定单次计算的精度,模数链决定能算多深,明文槽决定一次能并行算多少。所有参数配置都是在三者之间找平衡。
3. 为什么“整个大模型加密”在工程上并不现实
3.1 算一笔账:7B模型密文化的资源占用
网上有人畅想“把Llama直接部署成同态加密版本”,这个说法听着性感,但你先算一笔账:7B参数模型如果用fp16存储,光权重就是14GB。CKKS密文膨胀系数通常在20到50倍,保守按30倍算,14GB权重加密完就是420GB,这还只是权重,没算中间激活值。
420GB什么概念?当下主流数据中心的GPU显存是80GB到192GB。就算你有四块A100拼接,权重放进去之后中间计算也完全放不下。更麻烦的是,密文计算时中间结果同样膨胀,每一层激活值都按几十倍体积增长,很快爆内存。我的实测经验是,哪怕把小块权重加密后做一次普通矩阵乘法,内存开销也是明文的几十倍起跳。
所以结论很直接:现阶段把完整大模型整包加密,工程上不可行。这不是同态加密算法不行,而是密文膨胀率和计算速度还没有跟上来。明白了这件事,落地策略就会从“全加密”转向“分层加密”,具体做法在3.3节讲。
3.2 非线性激活函数在密文上的代价
Transformer里到处都是ReLU、GELU、Softmax这些非线性函数,这是同态加密落地大模型的第二道坎。CKKS本身支持加法和乘法,但ReLU这种分段函数没法直接在密文上原样执行,只能找多项式近似,比如用x^3或更高阶多项式去拟合ReLU。近似就会引入误差,层数一多误差累积,最终结果可能完全偏离正确分类。
Softmax更麻烦,里面有个指数运算,在密文上做指数要展开成很长的多项式,乘法深度瞬间抬高。前面说过模数链是有限资源,一次Softmax可能吃掉大半条预算。实际项目中,我见过有人用平方函数近似ReLU,虽然损失一点准确率,但乘法深度少了两层,速度和精度权衡下来反而更实用。
所以结论是:大模型的非线性结构决定了它不能直接塞进HE电路。要么换一个更简单的模型结构来承载部分敏感计算,要么把非线性部分留给明文端做,只把线性部分放到密文上。
3.3 务实的落地形态:分层加密与混合推理
既然整体加密不可行,那实际能落地的形态是“分层加密、混合推理”。先把模型按敏感程度和计算类型拆开,只有最需要保护的部分进入密文域,其余部分保持明文计算。
我试过比较可行的三种切法。第一种,只加密输入嵌入层和输出层。用户输入先被加密,在密文上做embedding,得到的结果解密后进入明文Transformer,反向再加密输出层。这样用户的关键输入内容不会在明文链路里出现,但模型主体计算不受影响,性能损失相对可控。
第二种,把模型拆成端侧小模型加云端大模型的串行结构。端侧跑一个小模型负责特征提取和初筛,只把脱敏后的中间特征发给云端大模型。这个方案里敏感原始数据不出本地,云端只接触到低敏感度的高维特征。
第三种,把模型权重按模块加密,配合安全聚合用在联邦微调场景。推理时用明文大模型,训练时用HE保护梯度聚合,路径是通的,但距离成熟还有距离。
我见过很多团队在做第一种,因为它能用比较小的代价覆盖掉最痛的“用户提示词泄露”问题,后面的实践也围绕这种形态展开。
4. 用TenSEAL跑通一个最小密文推理实践
4.1 环境准备与上下文参数
实践环节我用了TenSEAL这个库。它是Microsoft SEAL的Python封装,API简单,适合快速验证推理链路,底层仍然是C++实现的CKKS方案。安装命令很简单:
pip install tenseal==0.3.14建议用Python 3.9或3.10,太新的Python版本有时会遇到预编译包缺失。另外需要numpy、scikit-learn、torch这些常规依赖,训练部分我直接用PyTorch。
创建CKKS上下文时需要指定三个关键参数:多项式模度、模数链、全局缩放因子。我实测下来,微型模型用下面的配置足够跑两层矩阵乘法:
import tenseal as ts context = ts.context( ts.SCHEME_TYPE.CKKS, poly_modulus_degree=8192, coeff_mod_bit_sizes=[60, 40, 40, 40, 60] ) context.generate_galois_keys() context.global_scale = 2**40这里的乘法深度预算大概是3层左右,poly_modulus_degree越大能容纳的数据量越大,但计算也越慢。第一次入门建议就用8192,别一上来追求大规模,先把链路走通。
4.2 明文训练一个小型分类模型
为了演示HE推理,我用鸢尾花数据集训练一个两层MLP,输入4维特征,中间层8个神经元,输出3个类别。这个模型足够小,权重可以直接转成明文向量用于加密推理。
from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler import torch import torch.nn as nn data = load_iris() X, y = data.data, data.target X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler().fit(X_train) X_train = scaler.transform(X_train) X_test = scaler.transform(X_test) model = nn.Sequential( nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 3) ) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.01) model.train() for epoch in range(200): inputs = torch.tensor(X_train, dtype=torch.float32) labels = torch.tensor(y_train, dtype=torch.long) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step()训练完之后要把模型设为eval模式并取出权重,这是后面加密推理的输入。不要带着梯度和BN层,所有参数必须是纯线性层的权重和偏置,否则后续转换会非常麻烦。
4.3 把权重和输入搬进密文
推理前需要把明文输入加密,同时把训练好的权重转成明文向量列表。这一步是整个实践的核心,代码逻辑很直观,但你会在打包细节上踩不少坑。
import numpy as np model.eval() W1 = model[0].weight.detach().numpy() # shape (8, 4) b1 = model[0].bias.detach().numpy() # shape (8,) W2 = model[2].weight.detach().numpy() # shape (3, 8) b2 = model[2].bias.detach().numpy() # shape (3,) def encrypted_forward(context, x, W1, b1, W2, b2): enc_x = ts.ckks_vector(context, x) # 第一层线性变换 enc_h = enc_x.dot(W1.transpose()) + b1 # 用平方函数近似ReLU enc_h = enc_h * enc_h # 第二层线性变换 enc_logits = enc_h.dot(W2.transpose()) + b2 return enc_logits x_test = scaler.transform([[5.1, 3.5, 1.4, 0.2]]).flatten() enc_logits = encrypted_forward(context, x_test, W1, b1, W2, b2) decrypted_logits = enc_logits.decrypt()解密后的结果应该接近明文模型对同一个输入的输出。第一次跑通时我吃了一惊,真的能在“看不到输入内容”的情况下算出几乎一样的分类置信度。注意dot方法执行的是内积操作,W1.transpose()负责把权重矩阵转成按行计算的形式,这一步需要保证维度对齐,很多报错都出在这。
4.4 结果验证与误差意识
我实测下来,明文推理一次大约是微秒级,而同样的计算在密文上跑,单条样例大概要1到3秒。减速比在1000倍以上,这就是同态加密的现实代价。
解密结果和明文结果对比时,误差通常在1e-3量级,分类结果是稳定的。CKKS是近似方案,允许日常服务里出现这种误差,但不能接收误差无限增长。如果发现误差到了0.1甚至更大,基本可以判定是参数配置有问题,重点检查scale和模数链是否匹配。
这个最小实践虽然只覆盖了两层线性网络,但增删网络层、修改损失函数、更换数据集都通用。核心要记住链路是五步:创建上下文、加密输入、密文矩阵乘法、多项式近似激活、解密校验。后续任何更复杂的模型都可以按这个模式扩展。
5. 实测避坑:我踩过的四个典型坑
5.1 解密结果变成噪声:噪声预算计算失误
第一次跑通之前,我先试了一个三层MLP,结果解密出来的数字千奇百怪,甚至有负数几十亿。查了一圈发现不是代码bug,而是模型乘法深度超过了上下文支持的深度。
CKKS每次乘法都会消耗噪声预算,enc_h * enc_h这一步已经把预算用掉一大半,后续再加一层矩阵乘法就直接崩了。解决办法有两个方向:要么减少乘法层数,要么增加模数链的长度。我的做法是在程序里打印剩余噪声预算:
print("remaining noise budget:", context.remaining_noise_budget())这个数字低于20的时候,基本可以断定结果会出错。开始设计模型前,先用手画一遍计算图,数清楚有几个乘法层,再决定coeff_mod_bit_sizes的长度。我就是凭这个习惯避开了后面大半的废操作。
5.2 内存被密文体积撑爆:打包与分批
看代码示例时,一个普通向量加密后占用几十KB,你不会觉得大。但一旦输入从4维变成128维,中间层的神经元数变成256,同时处理100条请求,内存直接爆掉。我第一次尝试同时加密32个特征向量时,操作系统开始疯狂swap,程序卡死。
解决套路是打包。CKKS的明文槽支持SIMD,把多个样本的同一维度塞进同一个密文的不同槽里,让一次密文计算同时处理多条数据。实际写起来要处理槽对齐、mask、旋转,代码复杂度上一个台阶。我的建议是入门阶段不碰复杂打包,先用单条输入跑通流程,等真正要上线时再研究批量打包,否则调试成本会高到怀疑人生。
另一个曲线救国方案是,把权重矩阵按对角线打包成若干个向量,把矩阵乘法拆成多次向量内积。这个方案代码量更少,但乘法次数会增加不少,需要权衡。
5.3 精度莫名其妙丢失:scale选型与重缩放
类似模型结构,换一组参数之后,解密结果的精度突然从1e-3掉到1e-1,原因通常是scale设得太小。CKKS里浮点数编码本身就带近似误差,scale太小,编码时小数部分就被截断掉了,解密出来的结果跟明文对不上。
我后来形成一套固定配置:模型深度三到五层时,scale用2的40次方,模数链每级大小按[60, 40, 40, 40, 60]这种结构排。小模型用这个配置基本不会翻车。更稳的做法是在正式加密推理前,先用“明文加噪声仿真”跑一遍业务逻辑。就是直接把每一次运算结果人为加上高斯噪声,模拟HE的数值误差曲线。这个方法成本很低,能提前判断整个链路能否容忍近似误差,比反复跑真加密快得多。
精度问题还有一个隐藏点:激活函数近似。用平方函数替代ReLU,分类边界会变形,可能丢掉一两个百分点的准确率。如果你不能接受,可以升级到三阶或五阶多项式近似,代价是乘法深度加大,速度更慢。
5.4 “加密了模型”不等于“一切安全”
这是认知上的坑,也是我特别想提醒的。很多同学以为把权重和输入都加密了,整个系统就固若金汤,实际远不是这样。即使模型权重是密文,攻击者依然能通过输入输出对做差分攻击,判断模型边界和提取部分信息。日志系统如果记录了中间密文和解密结果,也可能被用来做侧信道攻击。
我的建议是:把同态加密当成隐私防御体系里的一块砖,而不是全部。日志脱敏、访问控制、审计、模型水印、输入输出频率限制这些传统手段一个都不能少。我见过不少团队做完HE加密之后沾沾自喜,结果一个未加密的API日志把用户输入全暴露了,那加密就没有意义。
6. 现阶段最值得尝试的技术路线与优化方向
6.1 三种可以工程落地的组合方案
如果你看完上面的内容,准备开始做隐私优先的大模型应用,我给三个可以落地参考的组合方案。
方案A:私密输入加明文大模型。用户输入加密后进入云端,云端在密文上完成embedding和第一层线性变换,中间结果交互解密后再走明文Transformer,输出层再做加密保护。适合“不想让模型服务方看到用户具体查询内容”的场景,比如医疗咨询、法律文书分析。这个方案改动相对小,只需在前处理和后处理加两层HE计算。
方案B:私密模型加公钥推理。把经过微调的模型权重加密部署到第三方云,用户持有公钥对输入加密发起推理,云服务商无法获取模型原始权重。适合模型版权保护、AI模型交易场景。缺点是目前只能支持较小模型或模型的分片部分,整体交付还需要时间。
方案C:联邦微调加同态加密梯度聚合。多个数据持有方各自用本地数据微调模型,只把加密后的梯度上传到中心服务器,中心服务器在密文上聚合,实现“数据不出域、模型共同优化”。这条路更适合金融、医疗联盟场景,推理部分仍然走明文,训练阶段隐私问题被HE覆盖。
6.2 性能优化的几个核心方向
现在HE和大模型的性能差距确实大,但这不是死路。我看好几个方向,已经有实际进展。
第一个是SIMD打包的工程化。把多个样本、多个矩阵元素塞进同一批密文槽,是性价比最高的优化方式,能把推理吞吐量提升一到两个数量级。开源社区已经有基于TenSEAL的批量推理封装轮子,值得关注。
第二个是GPU加速。CKKS的底层运算是多项式乘法和数论变换,这些操作在GPU上的并行化效果很好。OpenFHE和部分商业库已经支持GPU后端,实测矩阵乘法的密文计算速度能提升几十倍。
第三个是专用硬件。FPGA和ASIC实现同态加密已经有了原型原型芯片,把大数乘法、模运算这些重计算放进硬件,功耗和延迟都更可控。虽然离大规模商用还有距离,但方向已经非常明确。
第四个是模型轻量化与HE协同。把大模型做剪枝、量化和蒸馏,得到一个小而精的敏感计算模块,再把这部分放到HE域里加密执行。思路和端云协同一致:大模型负责通用能力,小模型负责敏感数据的低风险处理。
6.3 我的第一版实践建议
如果你准备在自己的项目里试水,我的建议很直接:不要一上来就追求把完整大模型加密,先把“最小泄露面”梳理出来,把最痛的那一个环节用HE保护起来,再逐步扩大加密范围。
我踩过的弯路里,最典型的就是想一步到位做全链路加密,花了两周时间调参数,最后发现模型权重太大、激活函数太复杂,根本推不动。调整策略之后,先用一个端侧小模型处理原始敏感输入,再让云端大模型处理脱敏特征,项目很快就有了可演示的版本。
另一个经验是先跑“模拟密文”再跑真密文。你在明文上模拟加噪声和延迟,先把业务逻辑、API接口、异常处理全部写对,最后换真的HE后端,这样调试成本会低非常多。
我第一次跑通整个链路时最大的收获,不是学会了TenSEAL的API,而是理解了“隐私优先”在大模型落地中不是一个开关,而是一条路径。先把最容易出事的用户输入保护起来,再逐步把权重、中间梯度纳入保护范围,这条路走得通,而且越走越顺。如果你也在做类似的事情,建议从这篇里的最小实践开始跑一遍,然后回到自己的业务场景里,找出那个最需要加密的环节,先把它保护起来。