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