加密實(shí)戰(zhàn)指南:從原理到參數(shù)調(diào)優(yōu))
1. 為什么是 CKKS它解決了同態(tài)加密的哪塊短板1.1 從整數(shù)到浮點(diǎn)BFV/BGV 的天然局限很多剛接觸全同態(tài)加密的人第一次跑通的是 BFV 或 BGV 方案因?yàn)樗鼈冞壿嬛卑装岩粋€(gè)整數(shù)當(dāng)作多項(xiàng)式系數(shù)加密加法和乘法在密文上精確對(duì)應(yīng)明文整數(shù)運(yùn)算。至少在數(shù)學(xué)層面這套邏輯非常干凈。但一旦你想拿它做點(diǎn)真實(shí)業(yè)務(wù)問(wèn)題立刻冒出來(lái)——真實(shí)業(yè)務(wù)里幾乎全是浮點(diǎn)數(shù)。模型的權(quán)重是 0.731、1.2047 這種小數(shù)統(tǒng)計(jì)指標(biāo)是平均值、方差距離計(jì)算是歐氏距離。BFV/BGV 不是不能處理小數(shù)而是處理起來(lái)非常別扭。你得給每個(gè)數(shù)手動(dòng)乘一個(gè)縮放因子把它變成整數(shù)然后祈禱乘法次數(shù)不多。因?yàn)槊孔鲆淮纬朔s放因子的量級(jí)會(huì)指數(shù)膨脹你不得不算著位寬定期做截?cái)嗌圆涣羯窬染捅懒?。我?dāng)時(shí)第一次嘗試用 BFV 模擬一個(gè)簡(jiǎn)單的線性回歸推理模型只有 3 個(gè)特征、1 層乘法還能勉強(qiáng)跑通。但換成 2 層神經(jīng)網(wǎng)絡(luò)中間要經(jīng)過(guò)激活函數(shù)、多層矩陣乘法縮放因子的管理直接失控。那段時(shí)間我最大的感受是方案在數(shù)學(xué)上沒(méi)問(wèn)題工程上幾乎沒(méi)法用。CKKS 就是沖著這個(gè)痛點(diǎn)來(lái)的。1.2 近似計(jì)算思路把誤差當(dāng)特性而不是缺陷CKKS 全稱是 Cheon-Kim-Kim-Song按 2017 年論文的名字翻譯過(guò)來(lái)就是“近似數(shù)算術(shù)的同態(tài)加密”。它和 BFV/BGV 最本質(zhì)的區(qū)別在于CKKS 主動(dòng)承認(rèn)計(jì)算結(jié)果是近似的誤差是設(shè)計(jì)的一部分而不是方案的副作用。它的做法可以類比成定點(diǎn)數(shù)運(yùn)算把一個(gè)浮點(diǎn)數(shù)放大 2^40 倍變成一個(gè)大整數(shù)參與環(huán)上的運(yùn)算做完后再縮小回去。每次乘法后所有數(shù)據(jù)自動(dòng)進(jìn)入一個(gè)新的縮放級(jí)別這個(gè)機(jī)制在方案層面被固定下來(lái)叫作 rescale。你不用像用 BFV 那樣手動(dòng)維護(hù)縮放因子也不用擔(dān)心哪天忘了截?cái)鄬?dǎo)致整數(shù)溢出。方案內(nèi)部就把“小數(shù)點(diǎn)位置”管理好了。解密時(shí)你得到的不再是精確的明文而是“明文 一個(gè)小噪聲”。這個(gè)噪聲在參數(shù)合理時(shí)非常小比如小數(shù)點(diǎn)后 10 位以內(nèi)。對(duì)絕大多數(shù)機(jī)器學(xué)習(xí)推理、統(tǒng)計(jì)分析場(chǎng)景來(lái)說(shuō)這個(gè)精度完全夠用。這套設(shè)計(jì)思想在密碼學(xué)界其實(shí)很激進(jìn)。傳統(tǒng)同態(tài)加密追求的是“密文上算的和明文上算的完全一致”CKKS 直接把這個(gè)目標(biāo)改成了“足夠接近”。恰好現(xiàn)實(shí)世界里有大量計(jì)算本身就不需要精確結(jié)果。1.3 CKKS 的適用邊界CKKS 不是萬(wàn)能藥。它擅長(zhǎng)浮點(diǎn)計(jì)算、向量批量處理、深度較大的算術(shù)電路但不擅長(zhǎng)精確比較、取整、求余這類操作。金額結(jié)算這種一分錢都不能差的場(chǎng)景千萬(wàn)別用 CKKS老老實(shí)實(shí)回去用 BFV 或者別做同態(tài)。此外CKKS 的 bootstrapping自舉雖然已經(jīng)實(shí)用化但開(kāi)銷依然很高能通過(guò)參數(shù)設(shè)計(jì)避開(kāi)就避開(kāi)。適合用 CKKS 的場(chǎng)景主要有幾類隱私保護(hù)的機(jī)器學(xué)習(xí)推理輸入是密文模型參數(shù)可以明文也可以密文聯(lián)邦學(xué)習(xí)場(chǎng)景下的安全聚合把各參與方的梯度加密后聚合醫(yī)療數(shù)據(jù)、基因數(shù)據(jù)的加密統(tǒng)計(jì)分析金融風(fēng)控中的加密查詢或加密評(píng)分這篇文章后面會(huì)圍繞原理、實(shí)操、參數(shù)調(diào)優(yōu)和踩坑鏈路展開(kāi)。如果你是第一次接觸 CKKS建議先跑通最小代碼再回頭看數(shù)學(xué)如果你已經(jīng)在跑 CKKS 但精度不對(duì)、性能太差可以直接跳到第 5 章。2. 拆開(kāi) CKKS 引擎編碼、縮放與噪聲預(yù)算2.1 復(fù)數(shù)向量如何“裝進(jìn)”多項(xiàng)式所有基于 RLWE環(huán)學(xué)習(xí)錯(cuò)誤問(wèn)題的同態(tài)加密方案都工作在同一個(gè)代數(shù)結(jié)構(gòu)上多項(xiàng)式環(huán) R Z[X]/(X^N 1)。這里 N 是 2 的冪常見(jiàn)取值是 8192、16384、32768。明文、密文、密鑰全都是這個(gè)環(huán)上的元素也就是次數(shù)小于 N 的整數(shù)多項(xiàng)式。CKKS 的巧妙之處在編碼環(huán)節(jié)。雖然在環(huán)上只能處理整數(shù)多項(xiàng)式但 CKKS 能把一個(gè)長(zhǎng)度為 N/2 的復(fù)數(shù)向量編碼進(jìn)一個(gè)多項(xiàng)式里。大致過(guò)程是這樣的你有一個(gè)復(fù)數(shù)向量 z長(zhǎng)度是 N/2對(duì)它做共軛對(duì)稱擴(kuò)展變成一個(gè)長(zhǎng)度為 N 的向量對(duì)這個(gè)向量做逆傅里葉變換實(shí)際操作中就是逆 FFT把結(jié)果乘以縮放因子 Δ四舍五入成整系數(shù)多項(xiàng)式解碼就是逆過(guò)程多項(xiàng)式系數(shù)除以 Δ做傅里葉變換取前 N/2 個(gè)分量。為什么必須做共軛對(duì)稱擴(kuò)展因?yàn)槟娓道锶~變換的結(jié)果天然是關(guān)于原點(diǎn)共軛對(duì)稱的而我們要編碼的向量只有 N/2 個(gè)自由度必須保證編碼后的多項(xiàng)式落在環(huán)的某個(gè)特殊子空間里解密后取回的那 N/2 個(gè)值才剛好是原始數(shù)據(jù)。我第一次看這個(gè)編碼過(guò)程時(shí)繞了好久才轉(zhuǎn)過(guò)彎來(lái)。它本質(zhì)上就是“把數(shù)據(jù)藏進(jìn)系數(shù)的頻域表示里”所以 CKKS 里對(duì)數(shù)據(jù)的操作最后都會(huì)對(duì)應(yīng)到頻域上的逐點(diǎn)操作。這也是為什么后來(lái)打包、旋轉(zhuǎn)、矩陣乘法這些操作都有非常優(yōu)雅的實(shí)現(xiàn)方式。有個(gè)細(xì)節(jié)容易忽略編碼時(shí)是逆 FFT不是逆 NTT。雖然 BFV 也做類似變換但那是為了快速多項(xiàng)式乘法用的是模素?cái)?shù)域上的 NTTCKKS 編碼是真正在復(fù)數(shù)域上做傅里葉變換。理解這一點(diǎn)你就明白為什么 CKKS 天然支持復(fù)數(shù)而不只是實(shí)數(shù)了。2.2 scale 機(jī)制為什么乘法后一定要 rescalescale 是 CKKS 里最重要的概念沒(méi)有之一。你可以把 scale 理解為定點(diǎn)數(shù)里的小數(shù)點(diǎn)位置。編碼時(shí)乘的那個(gè) Δ 就是初始 scale典型值設(shè)成 2^40也就是 40 位二進(jìn)制精度大約相當(dāng)于 12 位十進(jìn)制小數(shù)。兩個(gè) scale 為 Δ 的密文相加結(jié)果的 scale 還是 Δ沒(méi)問(wèn)題。但兩個(gè)密文相乘時(shí)數(shù)學(xué)上對(duì)應(yīng)的是兩個(gè)多項(xiàng)式乘積明文的乘積自然變成了“明文 × 明文”它的 scale 變成了 Δ2。如果不做任何處理再來(lái)一次乘法就變成 Δ3再來(lái)幾次系數(shù)大小直接突破模數(shù)上限數(shù)據(jù)徹底損壞。CKKS 的方案機(jī)制是每次乘法后做一個(gè) rescale 操作把密文的每個(gè)系數(shù)除以 Δ近似地同時(shí)把 scale 從 Δ2 降回 Δ。這樣下一輪乘法依然面對(duì)的是 scale 為 Δ 的密文整個(gè)計(jì)算可以無(wú)限繼續(xù)下去直到模數(shù)鏈耗盡。關(guān)鍵點(diǎn)rescale 在實(shí)現(xiàn)時(shí)不是真的做除法而是通過(guò)模數(shù)切換完成的。這就是為什么 CKKS 的系數(shù)模數(shù)必須設(shè)計(jì)成多個(gè)素?cái)?shù)的乘積q q0 × q1 × q2 × ... × qL每做一次 rescale模數(shù)就換成除以一個(gè)素?cái)?shù)后的數(shù)scale 約等于去掉的那個(gè)素?cái)?shù)本身。這里的“約等于”就是 CKKS 近似性的來(lái)源之一因?yàn)樗財(cái)?shù)不可能恰好是 2 的冪所以每個(gè)素?cái)?shù)位的 scale 其實(shí)是 2^40 附近的一個(gè)數(shù)。我建議新手第一次寫代碼時(shí)把 scale、模數(shù)、rescale 這三者的關(guān)系用一張表寫下來(lái)初始 scale 是多少、乘法后變成多少、rescale 后模數(shù)剩多少、scale 回到多少。你會(huì)發(fā)現(xiàn)整個(gè)流程突然就清晰了。2.3 Level 與噪聲預(yù)算計(jì)算深度的天花板每個(gè) CKKS 密文都有一個(gè)“當(dāng)前處于模數(shù)鏈哪一層”的概念叫作 Level。初始密文在最高層 L每做一次 rescale 就降一層降到 0 就不能再做乘法了。所以模數(shù)鏈里素?cái)?shù)的數(shù)量直接決定了密文能承受的連續(xù)乘法次數(shù)。這就是為什么要做深度預(yù)估如果你的計(jì)算圖里最長(zhǎng)路徑是連乘 5 次模數(shù)鏈至少要預(yù)留 5 次 rescale 的余量通常再留 1 層作為精度緩沖。噪聲預(yù)算則是另一個(gè)維度。RLWE 密文里每個(gè)系數(shù)都帶有一個(gè)隨機(jī)小噪聲解密時(shí)需要用私鑰把噪聲“濾掉”才能看到明文。加法和乘法都會(huì)讓噪聲增長(zhǎng)而且乘法的噪聲增長(zhǎng)比加法快得多。當(dāng)累積噪聲超過(guò)某個(gè)閾值時(shí)明文就淹沒(méi)在噪聲里解密出來(lái)就是一坨亂碼??梢酝ㄟ^(guò) API 查看某個(gè)密文的噪聲預(yù)算。SEAL 里能看到初始噪聲預(yù)算大概是 log2(q) 減去某個(gè)常數(shù)。比如 coeff modulus 總位寬 240 位初始噪聲預(yù)算大概在 200 位左右每做一次乘法噪聲預(yù)算會(huì)掉幾十位。當(dāng)噪聲預(yù)算掉到接近 0加密就是廢的。實(shí)操中我習(xí)慣把“噪聲預(yù)算還有多少”作為判斷一次計(jì)算是否健康的第一指標(biāo)而不是先去看結(jié)果對(duì)不對(duì)。結(jié)果不對(duì)大概率是噪聲打穿了具體怎么排查第 5 章會(huì)展開(kāi)講。2.4 密鑰體系與運(yùn)算原語(yǔ)CKKS 的密鑰有三件套私鑰secret key解密用必須保密且只存在于一方公鑰public key加密用可以公開(kāi)分發(fā)求值密鑰evaluation key也叫重線性化密鑰relin key乘法后用來(lái)把密文尺寸從 3 個(gè)多項(xiàng)式壓縮回 2 個(gè)多項(xiàng)式求值密鑰的存在是因?yàn)閮蓚€(gè)密文相乘在數(shù)學(xué)上產(chǎn)生一個(gè)二次多項(xiàng)式直接展開(kāi)就太長(zhǎng)了。為了不破壞“密文固定由 2 個(gè)多項(xiàng)式組成”的結(jié)構(gòu)需要用重線性化技術(shù)在密鑰切換后把它壓回去。這個(gè)操作本身是性能大頭SEAL 庫(kù)里目前的實(shí)現(xiàn)已經(jīng)相當(dāng)快了但在大參數(shù)下依然可能占據(jù)大部分計(jì)算時(shí)間。除了加法和乘法CKKS 還支持密鑰切換key switching和旋轉(zhuǎn)rotation。旋轉(zhuǎn)是后面第 4 章要講的重點(diǎn)它是實(shí)現(xiàn)打包和矩陣運(yùn)算的基礎(chǔ)。旋轉(zhuǎn)需要額外的 rotation key用私鑰生成按需分發(fā)不能復(fù)用別的場(chǎng)景的 key。到這里CKKS 的核心機(jī)制已經(jīng)講清了它是一個(gè)支持近似浮點(diǎn)計(jì)算、具備完整加減乘和旋轉(zhuǎn)原語(yǔ)的同態(tài)加密方案。接下來(lái)直接進(jìn)入實(shí)操。3. 實(shí)機(jī)演練用開(kāi)源庫(kù)跑通第一個(gè) CKKS 計(jì)算3.1 庫(kù)選型對(duì)比從哪個(gè)庫(kù)入手成本最低業(yè)內(nèi)常用的 CKKS 實(shí)現(xiàn)庫(kù)有這幾個(gè)庫(kù)語(yǔ)言特點(diǎn)建議Microsoft SEALC文檔最全、社區(qū)最活躍、API 設(shè)計(jì)清晰首選適合系統(tǒng)學(xué)習(xí)OpenFHEC支持多方案從 PALISADE 演進(jìn)而來(lái)生產(chǎn)環(huán)境可考慮HEAAN / OpenHEEAANCCKKS 原作者團(tuán)隊(duì)的實(shí)現(xiàn)偏研究參考LattigoGo純 Go適合 Go 后端集成后端是 Go 時(shí)推薦TensealPython封裝 SEALAPI 更簡(jiǎn)單快速原型驗(yàn)證首選PyfhelPython封裝 SEAL老牌原型可以湊合用我的建議是正兒八經(jīng)理解 CKKS 用 SEAL這個(gè)庫(kù)的錯(cuò)誤提示比較友好參數(shù)校驗(yàn)嚴(yán)格能幫你省掉很多自我懷疑的時(shí)間。只是快速驗(yàn)證想法、不想碰 C那就用 Tenseal。3.2 環(huán)境準(zhǔn)備和初始化以 SEAL 4.x 的 C API 為例初始化的代碼骨架如下#include seal/seal.h using namespace seal; EncryptionParameters parms(scheme_type::ckks); size_t poly_modulus_degree 8192; parms.set_poly_modulus_degree(poly_modulus_degree); parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, {60, 40, 40, 60})); auto context SEALContext::Create(parms); KeyGenerator keygen(context); auto secret_key keygen.secret_key(); PublicKey public_key; keygen.create_public_key(public_key); RelinKeys relin_keys; keygen.create_relin_keys(relin_keys); Encryptor encryptor(context, public_key); Evaluator evaluator(context); Decryptor decryptor(context, secret_key); CKKSEncoder encoder(context); double scale pow(2.0, 40);這里的 poly_modulus_degree 是 N取 8192coeff modulus 配的是 60、40、40、60代表四個(gè)素?cái)?shù)每個(gè)大約 60 位或 40 位初始總模數(shù)約 200 位。這個(gè)配置是 SEAL 官方推薦的入門參數(shù)安全強(qiáng)度約 128 位最大乘法深度為 2。scale 取 2^40意味著編碼時(shí)保留 40 位左右的精度。注意coeff modulus 的第一個(gè)和最后一個(gè)素?cái)?shù)通常設(shè)得比其他大原因是第一個(gè)素?cái)?shù)影響初始噪聲預(yù)算最后一個(gè)素?cái)?shù)影響最終精度。中間區(qū)間的素?cái)?shù)大小就是每次乘法后 scale 的近似值這里設(shè)成 40 位和 scale 相呼應(yīng)。3.3 完整加密加乘流程假設(shè)我們要算 0.5 × 1.5明文域結(jié)果是 0.75現(xiàn)在全程在密文上完成Plaintext plain1, plain2; encoder.encode(0.5, scale, plain1); encoder.encode(1.5, scale, plain2); Ciphertext c1, c2; encryptor.encrypt(plain1, c1); encryptor.encrypt(plain2, c2); evaluator.multiply_inplace(c1, c2); evaluator.relinearize_inplace(c1, relin_keys); evaluator.rescale_to_next_inplace(c1); Plaintext plain_result; decryptor.decrypt(c1, plain_result); vectordouble result; encoder.decode(plain_result, result); cout result[0] endl; // 約 0.75這段代碼里有三個(gè)操作不能省略也不建議調(diào)整順序multiply_inplace密文相乘此時(shí) scale 變成 Δ2密文變成 3 個(gè)多項(xiàng)式relinearize_inplace用求值密鑰把 3 個(gè)多項(xiàng)式壓回 2 個(gè)rescale_to_next_inplace把 scale 從 Δ2 降回 Δ同時(shí) Level 減 1為什么先重線性化再 rescale因?yàn)橹鼐€性化是對(duì)當(dāng)前模數(shù)下的多項(xiàng)式做密鑰切換密文項(xiàng)數(shù)少一項(xiàng)后續(xù)計(jì)算量自然更小。反過(guò)來(lái)先 rescale 再重線性化也可以做到數(shù)學(xué)上等價(jià)但是后者需要先把三個(gè)多項(xiàng)式都切到低模數(shù)白白多算一輪不劃算。再說(shuō)一遍 scale 對(duì)齊的問(wèn)題兩個(gè)密文相加要求兩者的 scale 一致。如果一個(gè)是 2^40另一個(gè)乘過(guò)一次后 rescale 回 2^40那沒(méi)問(wèn)題。但如果一個(gè)密文剛乘過(guò)還沒(méi) rescalescale 是 2^80另一個(gè)是 2^40直接相加得到的結(jié)果 scale 是亂的后續(xù)再乘再 rescale 基本就廢了。很多精度問(wèn)題其實(shí)是這一步埋下的雷。3.4 解碼與精度觀察跑完上面這段代碼你大概率會(huì)得到一個(gè)類似 0.7499999999999 的結(jié)果。這個(gè)“差一點(diǎn)”就是 CKKS 的常態(tài)不是 bug。誤差來(lái)源有三塊編碼時(shí)把浮點(diǎn)數(shù)放大并取整這一步已經(jīng)損失了低于 2^-40 的尾數(shù)乘法過(guò)程中噪聲累積導(dǎo)致最低幾位抖動(dòng)rescale 時(shí)除以的素?cái)?shù)不是精確等于 2^40引入了微量偏差這三個(gè)誤差源里第一個(gè)是可預(yù)測(cè)的第二個(gè)可以通過(guò)參數(shù)控制第三個(gè)是方案固有屬性。實(shí)踐時(shí)不需要強(qiáng)求結(jié)果和明文完全一致只需要確認(rèn)誤差在應(yīng)用可接受范圍內(nèi)即可。我第一次跑通這個(gè)最小示例時(shí)盯著那個(gè) 0.7499999 看了半天一度懷疑是不是哪里寫錯(cuò)了還專門拿明文算了三遍。后來(lái)才意識(shí)到這正是 CKKS 的“近似算術(shù)”設(shè)計(jì)在起作用。搞清楚這個(gè)心理預(yù)期后面排查問(wèn)題會(huì)省很多力氣。4. 從單值到批量打包和旋轉(zhuǎn)的正確打開(kāi)方式4.1 插槽和 SIMD 打包CKKS 單個(gè)密文可以裝 N/2 個(gè)復(fù)數(shù)N 8192 時(shí)就是 4096 個(gè)。這些位置叫 slot插槽。對(duì)密文做一次乘法相當(dāng)于同時(shí)對(duì) 4096 個(gè)值做乘法開(kāi)銷和只算 1 個(gè)值幾乎一樣。這就是 CKKS 實(shí)用化的關(guān)鍵。全同態(tài)加密的性能再差一次操作能同時(shí)處理幾千個(gè)數(shù)據(jù)攤薄下來(lái)每個(gè)數(shù)據(jù)的成本就能降到可用范圍。所有認(rèn)真的項(xiàng)目幾乎都會(huì)用批處理技術(shù)把計(jì)算圖矢量化。編碼時(shí)直接把一個(gè) vectordouble 傳進(jìn)去即可vectordouble input {1.0, 2.0, 3.0, 4.0}; Plaintext plain; encoder.encode(input, scale, plain);解碼時(shí)拿出來(lái)的也是一個(gè) vector。要注意的是長(zhǎng)度必須小于等于 N/2多了編碼器會(huì)直接報(bào)錯(cuò)。這里有個(gè)常見(jiàn)的性能誤區(qū)不少人在初期只打包一個(gè)值結(jié)果性能慘不忍睹誤以為 CKKS 完全不可用。其實(shí)應(yīng)該養(yǎng)成一個(gè)習(xí)慣拿到一個(gè)計(jì)算任務(wù)先問(wèn)自己能往一個(gè)密文里塞多少個(gè)獨(dú)立樣本。能塞進(jìn)去性能問(wèn)題就小一半。4.2 旋轉(zhuǎn)操作密文里的位移批處理有一個(gè)繞不開(kāi)的配套操作旋轉(zhuǎn)。旋轉(zhuǎn)的作用是讓密文里的第 i 個(gè) slot 移動(dòng)到第 i1 個(gè)或任意偏移位置。為什么要旋轉(zhuǎn)因?yàn)楹芏鄷r(shí)候一個(gè)數(shù)據(jù)點(diǎn)的計(jì)算要跨 slot 訪問(wèn)其他數(shù)據(jù)。典型的例子是卷積。卷積核要掃過(guò)整個(gè)特征圖對(duì)于每個(gè)輸出位置都要把周圍鄰域的值取出來(lái)做加權(quán)和。相鄰位置的輸入在打包時(shí)存在不同 slot 里不旋轉(zhuǎn)根本取不到。SEAL 里旋轉(zhuǎn)需要先生成 rotation keyvectorint steps {1, -1, 2, -2}; // 旋轉(zhuǎn)步長(zhǎng)集合 RotationKeys rot_keys; keygen.create_rotations_keys(steps, rot_keys);之后對(duì)密文做旋轉(zhuǎn)evaluator.rotate_vector_inplace(c, 1, rot_keys); // 左移一個(gè) slot evaluator.rotate_vector_inplace(c, -1, rot_keys); // 右移一個(gè) slot生成 rotation key 時(shí)需要規(guī)劃好哪些步長(zhǎng)會(huì)用到。步長(zhǎng)越多樣key 文件越大生成時(shí)間也越長(zhǎng)。一個(gè)項(xiàng)目里用到 1、2、4、8 這種 2 的冪次步長(zhǎng)很常見(jiàn)既能覆蓋各種偏移又不會(huì)生成太多 key。旋轉(zhuǎn)操作的開(kāi)銷比乘法大不少因?yàn)樗鼉?nèi)部要做密鑰切換。實(shí)際優(yōu)化中要盡量把旋轉(zhuǎn)次數(shù)壓縮到最少比如把連續(xù)兩次旋轉(zhuǎn)合并成一次大偏移的旋轉(zhuǎn)。4.3 矩陣乘法的實(shí)現(xiàn)思路矩陣乘法是 CKKS 在機(jī)器學(xué)習(xí)推理中最常用的操作。假設(shè)輸入是向量 x權(quán)重是矩陣 W要算 y Wx。最常見(jiàn)的做法是對(duì)角線打包法diagonal packing把矩陣 W 拆成若干條“對(duì)角線”每條對(duì)角線打包進(jìn)一個(gè)明文把輸入向量 x 的對(duì)應(yīng)元素通過(guò)旋轉(zhuǎn)對(duì)齊到正確位置把每個(gè)對(duì)角線的乘積累加得到結(jié)果向量的密文具體來(lái)說(shuō)一個(gè) 4×4 矩陣可以拆成 4 條對(duì)角線每條對(duì)角線對(duì)應(yīng)一個(gè)偏移步長(zhǎng)。計(jì)算時(shí)對(duì)夾具輸入向量做對(duì)應(yīng)步長(zhǎng)的旋轉(zhuǎn)然后逐對(duì)角線做乘法和累加。整個(gè)過(guò)程只需要 4 次乘法、4 次旋轉(zhuǎn)和若干次加法。這個(gè)方法的優(yōu)點(diǎn)是乘法次數(shù)和旋轉(zhuǎn)次數(shù)都等于矩陣的“非零對(duì)角線數(shù)”遠(yuǎn)小于逐元素展開(kāi)的乘法次數(shù)。缺點(diǎn)是涉及大量旋轉(zhuǎn)而旋轉(zhuǎn)慢。如果矩陣有特殊結(jié)構(gòu)比如稀疏、低秩還能進(jìn)一步優(yōu)化。另一個(gè)思路是直接用 CKKS 的“明文矩陣”乘法接口但通用性差一些。先跑通最基礎(chǔ)的對(duì)角線打包再根據(jù)實(shí)際矩陣形狀做定制優(yōu)化是比較合理的路徑。5. 參數(shù)調(diào)優(yōu)與精度事故的排查鏈路5.1 參數(shù)搭配的核心原則CKKS 的參數(shù)選擇會(huì)影響三個(gè)方面安全性、性能、精度。三者之間是矛盾關(guān)系需要根據(jù)應(yīng)用場(chǎng)景做取舍。我平時(shí)配置參數(shù)的順序是先確定需要的乘法深度 d。看計(jì)算圖里最長(zhǎng)的一條乘法鏈別只看層數(shù)要具體數(shù)每一層的乘法次數(shù)。一個(gè)深度為 d 再加初始精度的模數(shù)鏈通常至少需要 d 個(gè)“中間素?cái)?shù)”。再確定需要的精度也就是 scale 的位寬。普通機(jī)器學(xué)習(xí)推理用 2^40 足夠如果只需要少量乘法2^30 也能跑。scale 設(shè)得越高每個(gè)中間素?cái)?shù)就越大同樣位寬下能放的素?cái)?shù)數(shù)量就越少。綜合深度和精度算出 coeff modulus 的總位寬。再根據(jù)總位寬反推 poly_modulus_degree。SEAL 會(huì)直接告訴你某些參數(shù)組合不安全可以直接信任它的報(bào)錯(cuò)。舉個(gè)例子假設(shè)要跑 4 次乘法每次乘法后都需要做 scale 回落到 2^40模數(shù)鏈結(jié)構(gòu)可以是 {60, 40, 40, 40, 60}總位寬 240 位。這個(gè)總位寬下 poly_modulus_degree 至少要 16384因?yàn)?8192 能承載的安全模數(shù)上限大約只有 218 位左右。核心原則是不要自己造參數(shù)組合先用官方推薦的安全參數(shù)再逐步微調(diào)。SEAL 的CoeffModulus::Create會(huì)根據(jù) poly_modulus_degree 自動(dòng)判斷素?cái)?shù)位數(shù)是否過(guò)界最終的安全級(jí)別也可以從context里查出來(lái)。5.2 精度崩潰的排查鏈路CKKS 跑著跑著結(jié)果變得離譜這個(gè)問(wèn)題幾乎每個(gè)使用者都會(huì)遇到。我踩過(guò)幾次坑之后整理出一套固定排查順序按這個(gè)順序檢查通常能快速定位第一步查噪聲預(yù)算。解密失敗、結(jié)果完全錯(cuò)亂多半是噪聲打穿了。在關(guān)鍵操作前打印一下密文的噪聲預(yù)算看看是不是已經(jīng)掉到 20 位以下。如果是說(shuō)明乘法深度超出了模數(shù)鏈能支撐的上限。第二步查 scale 一致性。把所有加在一起的密文 scale 打出來(lái)看看是否一致。注意不是看數(shù)值接近程度而是看是否完全相等因?yàn)?2^40 和 2.0000000001^40 在后續(xù)乘法中的表現(xiàn)是截然不同的。加操作的兩個(gè)輸入 scale 不一致是最常見(jiàn)的 Bug。第三步查 rescale 是否遺漏。乘法之后是否做了 rescale如果連續(xù)做了幾次乘法而中間沒(méi)有 rescalescale 會(huì)指數(shù)增長(zhǎng)后果類似整數(shù)溢出。第四步檢查編碼端的明文值。解碼前先在明文域算一遍確認(rèn)期望值在合理范圍。有時(shí)候不是密文算錯(cuò)了而是初始數(shù)據(jù)編碼時(shí)就已經(jīng)引入了不可接受的誤差。第五步檢查模數(shù)鏈設(shè)計(jì)。乘法深度估計(jì)是否準(zhǔn)確中間素?cái)?shù)太小導(dǎo)致每次 rescale 精度掉得太快這些通常需要回到第 5.1 節(jié)重新推演。按這套順序排查絕大多數(shù)精度問(wèn)題都能在半小時(shí)內(nèi)找到根源比起對(duì)著代碼發(fā)呆效率高很多。5.3 性能優(yōu)化經(jīng)驗(yàn)CKKS 的性能瓶頸通常集中在三個(gè)地方多項(xiàng)式乘法NTT、重線性化密鑰切換、旋轉(zhuǎn)。前兩個(gè)是密碼學(xué)原語(yǔ)優(yōu)化空間不大但可以通過(guò)減少操作次數(shù)來(lái)降低總開(kāi)銷。旋轉(zhuǎn)的優(yōu)化空間相對(duì)更大。幾個(gè)實(shí)測(cè)下來(lái)效果明顯的優(yōu)化手段優(yōu)先做批處理。同樣的計(jì)算一個(gè)密文裝 4096 個(gè)值和裝 1 個(gè)值代價(jià)幾乎一樣。批處理是所有性能優(yōu)化的前提。減少計(jì)算圖里的乘法深度。有些數(shù)學(xué)公式可以改寫比如把多個(gè)乘法的連乘結(jié)構(gòu)改成加法樹(shù)結(jié)構(gòu)或者用乘法次數(shù)更少的算法實(shí)現(xiàn)多項(xiàng)式求值。把多個(gè)旋轉(zhuǎn)合并。如果計(jì)算中先后要旋轉(zhuǎn) 1 位和 2 位考慮是否可以直接旋轉(zhuǎn) 3 位一次到位。盡量用明文乘法。CKKS 支持密文乘明文這個(gè)操作比重線性化密文乘法快很多因?yàn)椴恍枰鼐€性化。模型推理時(shí)把權(quán)重作為明文輸入作為密文能省下大量開(kāi)銷。減少 key 的生成數(shù)量。rotation key 和 relin key 的生成并不便宜每個(gè) key 都要做密鑰切換的預(yù)處理。key 的管理和發(fā)送也要規(guī)劃好避免每個(gè)參與方都生成全套 key。一個(gè)典型的優(yōu)化案例我在做一個(gè) 3 層 MLP 推理時(shí)一開(kāi)始把模型權(quán)重也加密了整個(gè)推理耗時(shí) 6 秒。后來(lái)改成權(quán)重明文、輸入密文同樣結(jié)果耗時(shí)降到 1.8 秒。再通過(guò)打包把多個(gè)輸入樣本同時(shí)推理單樣本延遲降到幾十毫秒。性能和精度一樣都需要從全局視角去調(diào)整而不是指望某一個(gè)參數(shù)能救回來(lái)。6. 落地視角密文浮點(diǎn)計(jì)算在項(xiàng)目中的位置6.1 隱私機(jī)器學(xué)習(xí)推理的典型結(jié)構(gòu)CKKS 在隱私保護(hù)機(jī)器學(xué)習(xí)推理中的角色很清晰數(shù)據(jù)持有方把輸入加密成密文發(fā)給計(jì)算方計(jì)算方在密文上執(zhí)行模型推理返回加密結(jié)果只有數(shù)據(jù)持有方能解密看到結(jié)果。這里模型權(quán)重可以有兩種處理方式一種是明文計(jì)算方直接把模型參數(shù)以明文形式參與密文乘法效率高適用于“計(jì)算方擁有模型、數(shù)據(jù)方想用模型”的場(chǎng)景另一種是權(quán)重也加密適用于雙方都不希望對(duì)方知道模型參數(shù)的場(chǎng)景但計(jì)算開(kāi)銷明顯增大。實(shí)際工程中還有一種混合模式把神經(jīng)網(wǎng)絡(luò)的前幾層用 CKKS 做密文推理后面的層直接以明文形式在解密后進(jìn)行避免整個(gè)網(wǎng)絡(luò)都跑在密文域里。這帶來(lái)的安全邊界變化需要謹(jǐn)慎評(píng)估但確實(shí)是性能和安全的常見(jiàn)折中點(diǎn)。6.2 與其他隱私計(jì)算技術(shù)結(jié)合全同態(tài)加密不一定要單打獨(dú)斗。CKKS 擅長(zhǎng)浮點(diǎn)算術(shù)但在比較、取整、條件分支這些操作上很弱安全多方計(jì)算MPC恰好擅長(zhǎng)這些邏輯操作缺點(diǎn)是通信量大。兩者結(jié)合是現(xiàn)在工業(yè)界的主流思路之一。常見(jiàn)分工是大的浮點(diǎn)矩陣運(yùn)算交給 CKKS利用它的批處理和近似算術(shù)特性涉及比較、截?cái)?、ReLU 激活這類操作時(shí)用 MPC 協(xié)議輪換處理比如神經(jīng)網(wǎng)絡(luò)推理里的 ReLU 無(wú)法用 CKKS 高效實(shí)現(xiàn)一種做法是把 ReLU 近似成多項(xiàng)式全程留在 CKKS 域里另一種做法是通過(guò)秘密分享切到 MPC 域里做精確比較再切回來(lái)。前者快但精度有損后者精確但慢。具體選哪種取決于模型對(duì)激活函數(shù)精度的敏感度。聯(lián)邦學(xué)習(xí)也是 CKKS 的典型搭配。各參與方本地訓(xùn)練后把梯度用 CKKS 加密上傳聚合方在密文上求平均再把結(jié)果返回解密。這個(gè)過(guò)程中聚合方全程接觸不到任何一方的明文梯度比單純傳輸梯度要安全得多。6.3 什么時(shí)候不該用 CKKS給項(xiàng)目做技術(shù)選型時(shí)知道“什么時(shí)候不選”比知道“什么時(shí)候選”更重要。以下幾種情況CKKS 大概率不是最優(yōu)解需要精確整數(shù)結(jié)果任何一位小數(shù)的偏差都不可接受回退到 BFV 更穩(wěn)妥計(jì)算主要是字符串處理、數(shù)據(jù)庫(kù)查詢、條件分支CKKS 完全不擅長(zhǎng)數(shù)據(jù)量極小、乘法深度極大、但只需要算一次自舉開(kāi)銷可能讓整體方案不劃算參與方之間可以高頻交互MPC 或兩方安全計(jì)算可能更高效只是對(duì)單條記錄做簡(jiǎn)單聚合也許差分隱私或可信執(zhí)行環(huán)境更合適CKKS 的價(jià)值在于“在不可信環(huán)境下完成浮點(diǎn)稠密計(jì)算”抓住這個(gè)定位選型和方案設(shè)計(jì)就不容易跑偏。還有一個(gè)常被忽略的點(diǎn)同態(tài)加密會(huì)放大一切計(jì)算成本甚至連“把結(jié)果發(fā)送回?cái)?shù)據(jù)方解密”這個(gè)環(huán)節(jié)都要設(shè)計(jì)好參與方。不是所有項(xiàng)目都需要端到端全程加密有些場(chǎng)景在特定節(jié)點(diǎn)解密后進(jìn)入明文處理會(huì)比強(qiáng)行全文加密更務(wù)實(shí)。最后關(guān)于入坑路徑的一點(diǎn)個(gè)人體會(huì)如果讓我給完全沒(méi)接觸過(guò) CKKS 的人一條學(xué)習(xí)路徑我的建議是不要先啃論文直接裝 SEAL把第 3 章的最小示例跑通然后打印每個(gè)階段的噪聲預(yù)算和 scale 變化體會(huì)一下“近似”到底是怎么一步步發(fā)生的。接著再回頭讀論文里的編碼和重線性化部分你會(huì)發(fā)現(xiàn)原本晦澀的數(shù)學(xué)突然變得好懂了。我見(jiàn)過(guò)不少同行卡在最開(kāi)始是因?yàn)槠谕?CKKS 像普通浮點(diǎn)運(yùn)算一樣“想當(dāng)然”。實(shí)際上只要接受三個(gè)前提——結(jié)果有噪聲、scale 要管理、乘法次數(shù)是硬資源——CKKS 用起來(lái)并不比傳統(tǒng)密碼學(xué)庫(kù)更復(fù)雜。等到你開(kāi)始獨(dú)立調(diào)參數(shù)、排查精度問(wèn)題時(shí)再去看一些深度優(yōu)化方案比如 bootstrapping、打包自動(dòng)編排這些進(jìn)階內(nèi)容的技術(shù)前提在前面已經(jīng)鋪好了。到那個(gè)階段你手邊至少應(yīng)該有一個(gè)能跑通的 CKKS 項(xiàng)目而不是停留在概念層面。