第90章 七千萬次的小石頭
白板前,尹航還在盯著那道拆盲盒題。
他的大腦正在高速運轉,試圖在這張錯綜複雜的概率網絡里找到哪怕一絲破綻。
姚思雨站在另一側,手裡握著板擦,已經把舊遞推公式毫不留情地劃掉。
江臨站在一旁,卻沒有繼續往下講。
這幾道題到這裡就已經足夠了。
阿里全球數學競賽預賽的題目,真正的價值從來不在於他把最終那個光禿禿的答案餵給別人,而在於看到那條線的過程。
先抽象。
把現實世界裡極其複雜的盲盒概率、卡池保底機制,剝離掉所有花哨的商業外衣,變成純粹的馬爾可夫鏈。
再壓縮。
把數以萬計的節點狀態,根據對稱性壓縮成一個只與剩餘未收集種類數相關的最小一維數組。
最後驗證。
用嚴密的邏輯去證明,這種降維壓縮沒有在任何一個微小的概率分岔口上丟掉必要信息。
尹航手裡的馬克筆在指尖轉了半圈,筆帽啪地一聲敲在掌心。
他轉過頭,用一種近乎看怪物的眼神看著江臨,忽然問:「你別告訴我,這十一道題,全都是像剛才這樣,一眼看穿底層結構,然後沒有任何卡頓地寫完的?」
江臨想了想,認真地搖了搖頭:「不全是,有一道代數變形題更偏純粹的計算和排版表達,沒有這麼強的結構感,純粹靠算力堆砌。而且,我也試了幾條錯路。」
尹航聽到錯路兩個字,肩膀猛地塌了下來,似乎長長地鬆了口氣:「靠,原來你也會走錯路啊,我還以為你腦子裡自帶量子計算機呢。」
「我走的錯路可能比你們任何人都多。」
江臨說著,還舉例說明將自己當初在一道數論題上被複雜的表象迷惑,走進死胡同的過程說了一遍。
「所以,你是找正確路線,排除錯誤方向,大概花了四十分鐘。後面用LaTeX整理提交版排版,花了一個多小時。」
尹航被江臨這番一本正經的訴苦氣笑了。
「謝謝你啊,你終於承認自己花了時間,終於像個人了。但我光是找這道盲盒題的路線,花的時間就是你的四倍,而且還沒找全。」
屋裡的氣氛終於徹底鬆弛了下來。
那種因為高強度智力對抗而緊繃的空氣,隨著尹航的自嘲煙消雲散。
姚思雨也忍不住笑了一下:「所以陸導讓你把B304當成自己家常來,不是沒有原因的。你在這裡,至少能讓我們知道,題目還能被拆到什麼程度,不至於剛摸到門檻就以為自己看見了天花板。」
邊上的孟澈見江臨開始收拾東西,知道他要回去,於是把下載好的幾份數據說明與論文列印稿遞過去,隨口問了一句:「最近除了阿里預賽,還在忙什麼,感覺你這個月都是神出鬼沒的。」
「寫一個數據檢查工具。」
「做什麼的?」
「量化相關吧。」
「你還搞量化?」孟澈訝然道。
他正在研究機器學習,很清楚量化金融領域的水有多深。
(由於緩存原因,請用戶直接瀏覽器訪問 海量小說在 TW 看書網,𝙨𝙝𝙪.𝙩𝙬任你讀 網站,觀看最快的章節更新)
「你才多大啊,你就搞量化?」尹航也是一副難以置信的模樣,「你別告訴我你已經開始炒股了。」
「也就是隨便看看,幫人做點基礎的數據梳理。」
但B304三人組顯然不相信。
一個能把阿里預賽十一道題當切菜一樣做完的人,嘴裡的隨便看看,絕對不可能是用Excel畫幾張K線圖那麼簡單。
不過三人見江臨沒有進一步解釋的意思,也就識趣地不去多問。
天才總有自己的秘密領域,這點默契他們還是有的。
傍晚,江臨伴著江城的晚霞回到家裡。
吃過晚飯,陪父母聊了會閒話,便回臥室忙自己的事情。
工作站電腦打開,有一封新郵件。
發件人:沈承業。
標題:QF-OLDLIB-001第一批脫敏材料已上傳
正文只有簡短的一句話:江臨,小型私募舊因子庫研究流程審計,正式開始,資源已開通。
江臨點擊連結,開始下載材料。
文件和之前他處理過的那些動輒十幾GB 的Tick級高頻行情數據比起來,不算太大,壓縮包只有不到3個G。
但在量化研究的語境裡,這種體量的信息密度是極其恐怖的。
解壓後,目錄樹展開。
幾張龐大的元數據表。
幾份回測配置摘要。
一個脫敏後的因子輸出矩陣。
一份很粗糙的歷史備註記錄。
還有一張極其關鍵的狀態表。
江臨點開狀態表。表里密密麻麻地列著四百三十七個因子編號。
在狀態這一列里:
有些後面寫著:active(目前仍在實盤交易中提供信號的因子)。
有些寫著:deprecated(因為績效衰減,已被淘汰的因子)。
有些寫著:merged(因為與其他因子相關性過高,被合併的因子)。
有些寫著:deleted(被刪除的因子)。
還有一批,令人觸目驚心地寫著:unknown(未知狀態,連研究員自己都不知道這東西還在不在跑)。
江臨的目光在deleted和unknown這兩個詞上停留了很久,思緒一下子發散開來。
在任何一個量化機構,乃至任何一個科研系統里,成功的東西,總會有人記得。
它們會被寫進PPT,會被掛在年報里,會被用來向投資人吹噓。
但是,失敗的東西,常常最先被刪掉。
而一個沒有失敗記錄的研究系統,最容易把倖存者當成真理。
在金融市場裡,這叫倖存者偏差。
當你只看到那些成功的策略時,你會覺得市場充滿規律。
但如果你看不到那99%因為過度擬合而在實盤中虧得血本無歸的廢棄因子,你就會在下一次研究中,重蹈覆轍。
江臨深吸了一口氣,熟練地打開終端,新建項目文件夾。
然後用Vim打開audit_log.md,敲下了這個審計項目的最高綱領。
第一行:舊因子庫首先不是寶庫,而是失敗記錄的墓地。
第二行:本項目不找神因子,不預測未來,只找「研究流程如何騙過自己」。
第三行:所有結論必須嚴格綁定四個維度:數據版本、樣本池版本、因子版本、回測配置版本。脫離版本談績效,一律視為學術造假。
寫完這三行,他才正式開始第一輪代碼層面的掃描。
最初的審計腳本邏輯並不複雜。
江臨用Python配合Pandas和Dask,寫了幾個守衛器。
唯一性檢查: 檢查因子編號是否唯一,版本號是否連續,每個因子是否嚴格綁定了當時所用的清洗數據版本。
樣本池漂移: 每個回測結果是否綁定了確定的樣本池定義?
失敗記錄完整性: 失敗因子有沒有保留詳細的刪除原因和失效日期的環境切片?
同源性檢查: 同名或近似同名因子是否重複出現?
摩擦成本檢查: 交易成本假設有沒有在不同版本里發生人為的漂移?
回車,運行。
十五分鐘後,第一輪掃描結果出來了。很難看,觸目驚心的難看。
四百三十七個因子裡,有八十多個沒有完整的數據版本號約束。這意味著如果現在重跑回測,根本不知道當初是用什麼數據跑出來的。
二十七個因子版本斷裂,從 v1.2 直接跳到了 v3.0,中間經歷了什麼,無人知曉。
十三個因子名稱完全不同,但備註里的數學邏輯高度相似。
還有幾個因子,名字看起來像是三個截然不同的方向。
但江臨的腳本對它們的脫敏輸出矩陣求了橫截面相關性後發現,它們的皮爾遜相關係數竟然高達0.98。
說明這根本就是同一類想法,在績效壓力和流程失控里被反覆複製、微調參數、改名重跑,試圖碰出一個更好看的夏普比率,最後在歷史裡留下了三具極其相似的影子。
這就是純粹的數據挖掘災難。
江臨面無表情,繼續跑第二輪:因子輸出版本敏感性測試。
第三輪:樣本池時間序列漂移測試。
第四輪:失敗因子記錄完整性穿透。
隨著任務越來越重,工作站腳下的機箱風扇開始持續低鳴,發出沉悶的嗡嗡聲。
江臨瞥了一眼副屏上的系統監控。
溫度:68℃,正常。
硬碟讀寫:正常。
內存占用:正常。
CPU 占用率,卻在幾個特定的腳本執行時,反覆拉高到100%,紅得刺眼。
程序並沒有卡死,終端里的進度條還在走。
但它比江臨預期的慢,慢得非常不合理。
對於現實世界裡的大多數數據科學家或者量化研究員來說,遇到這個情況,第一反應絕對是,機器不夠強。
「老闆,我們要買更好的 CPU,換 AMD 的線程撕裂者。」
「要加更大的內存,把數據全塞進內存里。」
「上AWS雲伺服器,開個 124 核的實例並行跑。」
用硬體的暴力去掩蓋軟體的低效,這是和平年代,資源充沛環境下的通病。
但這不是江臨的第一反應。
在他的腦海深處,那個屬於【廢土世界】的倒計時始終存在。
在資源枯竭的廢土裡,永遠沒有再買一台的選項。
在那裡,機器慢,不能先要資源,必須先問。為什麼慢?是什麼在吃掉算力?
江臨果斷按下了 Ctrl+C,停掉了後續的掃描任務。
打開Python的性能分析工具,將剛才那段跑得極慢的代碼用cProfile 重新包起來,然後接入SnakeViz進行可視化分析。
二十分鐘後,一份詳細的調用棧火焰圖出現在屏幕上。
真正的瓶頸浮出水面。
在調用棧里吞掉最多牆鍾時間的怪物,不是那些幾百萬行的大矩陣乘法。
不是複雜的HDF5文件讀取,不是前端渲染的圖表生成,也不是某個玄學的機器學習複雜模型。
而是幾個小得近乎不起眼的動作:排序、排名、分桶。
在量化回測中,經常需要在特定的行業特定的市值區間內,對股票的因子暴露值進行中性化和排序。
每次參與排序的股票可能不多,只有五個、八個、或者十六個。
單獨看,給五個數排序,不管用什麼算法,每一次都快得像沒有成本,連一毫秒都不需要。
但問題在於乘數效應。
QF-OLDLIB-001里有四百多個因子。
三年歷史版本。
多個動態調整的樣本池。
多種回測配置。
每一天,每一個行業,每一個狀態標記、每一個數據版本,都要切出一層層的橫截面進行分組排名。
於是,這些微小的小動作被嵌套在龐大的循環網裡,被反覆調用。
江臨把調用棧里耗時最高的那個函數次數列印了出來
七千八百四十二萬一千九百零六次。
看著這個天文數字,他沉默了十幾秒,然後在項目的audit_log.md里,他敲下了這樣一段話。
真正拖慢整個複雜系統的,從來都不是偶爾出現的大山,而是每天必須被搬運七千萬次的小石頭。
寫完這句話,他停頓了一下。
為了讓將來可能接手這份審計報告的平庸工程師也能看懂,他又補了一段更通俗的解釋。
一次給五張試卷按分數從高到低排序,對任何人都不難。
難的是,系統要求你一天之內,把給五張試卷排序這個動作,重複七千萬次。
標準庫里的排序算法在計算機科學上被證明是非常優秀的。
但它們優秀的前提是通用。
標準庫就像是一套占地幾萬平方米的大型自動化物流分揀中心。
它可以處理一萬張試卷。
可以處理一百萬個包裹。
可以處理帶有各種奇怪對象的複雜數據結構。
它強大,通用,絕對可靠。
但如果你的流水線上,每次送過來的永遠只有五個小包裹,而且每天要送七千萬次。
那麼,你每一次都去啟動那套耗電巨大的大型物流中心,讓傳送帶空轉,讓機械臂尋址,去執行龐大的分揀邏輯,
這就是不可饒恕的浪費。
在底層代碼的視角里,這種浪費體現為,為了通用性而保留的複雜的函數調用開銷。
為了處理多態而進行的動態類型檢查。
為了兼容不同數組長度,標準流程里保留了大量條件分支。
而這些分支一旦在熱點循環里反覆觸發,就會拖慢現代CPU最依賴的指令流水線。
事實上,並不是系統不會排。
而是流程太重了。
重到CPU的每一個時鐘周期都在被無意義的管理邏輯消耗。
江臨現在要做的,不是去推翻高德納在《電腦程式設計藝術》里寫下的經典排序理論,也不是發明什麼震驚世界的新算法。
他只是需要一套固定手勢。
五張試卷。
看第一張和第二張。
誰大誰在前面,該換就換。
再看第三張和第四張。
該換就換。
幾步極其固定的比較之後,順序自然就出來了。
不問多餘的類型問題。
不打開多餘的內存分配流程。
不為那根本不存在的一百萬張試卷準備任何冗餘的邊界檢查工具。
沒有數據相關的循環,沒有運行時臨時選擇路徑。
比較順序在編譯前就被釘死,剩下的只是固定位置之間的比較與交換。
只處理這五個位置的數字。
這就是在高性能計算領域裡,針對極其明確邊界的小規模數據,進行優化的核心奧義。
凌晨一點二十,萬籟俱寂,江臨在新建的C語言擴展文件里,寫下了第一版函數的簽名。
函數名很醜,甚至不像一個優雅算法庫里的東西。
rank5_fixed_v0。
它不試圖排序世界上一切數組,只處理五個float64因子暴露值,五個有效性標記,以及五個原始位置編號。
輸出的也不是一個漂亮的新數組,而是一組業務排名和一組mask。
它就像一把在廢土車間裡,為了擰某種特定型號引擎底盤上的特定螺絲,而被強行把手柄焊彎的怪異扳手。
但江臨現在需要的,正是這種專一暴力的扳手。
第一版寫完,江臨並沒有急著直接替換到Python 審計主流程里去。
作為在廢土裡見識過一個小數點錯誤導致整個證明功虧一簣的倖存者,他對替換底層邏輯有著病態的嚴謹。
他先做Baseline。
原流程輸出什麼,新函數就必須輸出一模一樣的東西。
在量化金融的數據里,現實永遠比理論骯髒。
無重複值(理想狀態)。
重複值(兩隻股票因子得分完全一樣)。
缺失值(NaN,某隻股票當天停牌沒有數據)。
極端值(Infinity)。
負值。
相等值且需要保持原相對順序(穩定排序要求)。
每一種情況,都必須對齊。
有重複值時,原來在數組裡誰在前面,現在排序後也必須誰在前面。
遇到NaN缺失值時,量化的規則不是數學上的把它當最大或者把它當最小,而是必須按照項目預設的規則,將其單獨剔除並打上MASK_NAN標籤,剩下的數繼續排。
這並非純粹數學定義上的排序,帶著強烈業務屬性的金融審計項目里的排名規則。
兩者絕不能混為一談。
凌晨兩點半,江臨揉了揉發酸的眼睛。
第一版v0跑過了包含兩萬個邊緣測試用例的單元測試。
結果全對。
速度有提升,但並不大,大概只快了15%。
第一版只是為了驗證這個強耦合的方向是走得通的。
江臨並不意外。
他重新打開C 代碼。
開始真正的榨乾性能。
刪掉所有不必要的狀態判斷。
把僅有的一點循環徹底展開,變成直線型的直線代碼。
把可能會出現的各種數據類型的可能性徹底焊死,限定在這個項目里真正會輸入進來的內存布局。
凌晨三點十八,第二版出來了。
他沒有把這個函數暴露成一個給Python循環逐次調用的小玩具。
那樣七千萬次跨語言調用本身就會成為新的災難。
他真正寫的是一個批處理入口。
一次性接收連續內存里的數百萬個五元組,在C層內部跑完整個固定排序網絡,再把排名矩陣吐回給Python。
再次跑測試。
結果一致。
速度提升到了30%。
但這還不夠。
江臨的眉頭微微皺起,他感覺到代碼里還有多餘的脂肪。
從抽屜里拿出一張白紙和一支筆。
紙上,他畫了五個圓圈,標上序號:0,1,2,3,4。
然後開始在圓圈之間連線。
他現在寫的不是代碼,而是動作。
在底層的彙編指令里,比較並交換是一個極其廉價的動作。
只要沒有if分支造成的預測失敗,指令就可以像水一樣順暢地通過 CPU流水線。
比較0和1(大的去右邊)。
比較3和4。
比較2和4。
比較2和3。
比較1和4。
比較0和3。
……
每一步,都像是在進行一次精密的機械手工調整。
五個數排好序,根本不需要程序在每一次運行的時候去思考接下來該怎麼辦。
路線是可以提前定死的。
就像水流經過預先挖好的迷宮溝渠,無論水勢大小,最終都會從既定的出口按照大小順序流出。
只要這套網格路線上,所有可能的 $5! = 120$ 種初始排列,最後都能被正確地疏導成有序狀態,就足夠了。
這就是計算機科學中極其冷門但極其硬核的概念。
排序網絡。
它一點也不聰明。
面對1000個數它毫無辦法。
但它非常穩定。
它不通用,但它是為高頻,小規模任務量身定製的終極殺器。
寫到這裡,江臨停下了筆。
忽然想起了上午在B304里,那個放在桌角的磁性幾何魔方,想起了那道阿里數學競賽的盲盒題。
他記得自己對尹航說過的話,先別被圖案牽著走,先找上界,再找取等構造。
優化排序底層的邏輯也一樣。
先別被排序這個在教科書里被講爛了的大詞嚇住。
他現在要處理的,根本不是排序學,只是五個內存位置之間有限次比較交換組合的最小充分集。
這是一個很小的世界。
小到它的所有狀態都可以被數學窮盡。
小到它是可以被嚴格證明的。
但它重要到,只要這個動作被重複七千萬次,它就會變成拖垮龐大金融系統的結構性瓶頸。
凌晨四點十分,窗外的天際已經隱隱有了一絲青灰色。
江臨敲下最後一組指令,第三版完成。
他的大腦正在高速運轉,試圖在這張錯綜複雜的概率網絡里找到哪怕一絲破綻。
姚思雨站在另一側,手裡握著板擦,已經把舊遞推公式毫不留情地劃掉。
江臨站在一旁,卻沒有繼續往下講。
這幾道題到這裡就已經足夠了。
阿里全球數學競賽預賽的題目,真正的價值從來不在於他把最終那個光禿禿的答案餵給別人,而在於看到那條線的過程。
先抽象。
把現實世界裡極其複雜的盲盒概率、卡池保底機制,剝離掉所有花哨的商業外衣,變成純粹的馬爾可夫鏈。
再壓縮。
把數以萬計的節點狀態,根據對稱性壓縮成一個只與剩餘未收集種類數相關的最小一維數組。
最後驗證。
用嚴密的邏輯去證明,這種降維壓縮沒有在任何一個微小的概率分岔口上丟掉必要信息。
尹航手裡的馬克筆在指尖轉了半圈,筆帽啪地一聲敲在掌心。
他轉過頭,用一種近乎看怪物的眼神看著江臨,忽然問:「你別告訴我,這十一道題,全都是像剛才這樣,一眼看穿底層結構,然後沒有任何卡頓地寫完的?」
江臨想了想,認真地搖了搖頭:「不全是,有一道代數變形題更偏純粹的計算和排版表達,沒有這麼強的結構感,純粹靠算力堆砌。而且,我也試了幾條錯路。」
尹航聽到錯路兩個字,肩膀猛地塌了下來,似乎長長地鬆了口氣:「靠,原來你也會走錯路啊,我還以為你腦子裡自帶量子計算機呢。」
「我走的錯路可能比你們任何人都多。」
江臨說著,還舉例說明將自己當初在一道數論題上被複雜的表象迷惑,走進死胡同的過程說了一遍。
「所以,你是找正確路線,排除錯誤方向,大概花了四十分鐘。後面用LaTeX整理提交版排版,花了一個多小時。」
尹航被江臨這番一本正經的訴苦氣笑了。
「謝謝你啊,你終於承認自己花了時間,終於像個人了。但我光是找這道盲盒題的路線,花的時間就是你的四倍,而且還沒找全。」
屋裡的氣氛終於徹底鬆弛了下來。
那種因為高強度智力對抗而緊繃的空氣,隨著尹航的自嘲煙消雲散。
姚思雨也忍不住笑了一下:「所以陸導讓你把B304當成自己家常來,不是沒有原因的。你在這裡,至少能讓我們知道,題目還能被拆到什麼程度,不至於剛摸到門檻就以為自己看見了天花板。」
邊上的孟澈見江臨開始收拾東西,知道他要回去,於是把下載好的幾份數據說明與論文列印稿遞過去,隨口問了一句:「最近除了阿里預賽,還在忙什麼,感覺你這個月都是神出鬼沒的。」
「寫一個數據檢查工具。」
「做什麼的?」
「量化相關吧。」
「你還搞量化?」孟澈訝然道。
他正在研究機器學習,很清楚量化金融領域的水有多深。
(由於緩存原因,請用戶直接瀏覽器訪問 海量小說在 TW 看書網,𝙨𝙝𝙪.𝙩𝙬任你讀 網站,觀看最快的章節更新)
「你才多大啊,你就搞量化?」尹航也是一副難以置信的模樣,「你別告訴我你已經開始炒股了。」
「也就是隨便看看,幫人做點基礎的數據梳理。」
但B304三人組顯然不相信。
一個能把阿里預賽十一道題當切菜一樣做完的人,嘴裡的隨便看看,絕對不可能是用Excel畫幾張K線圖那麼簡單。
不過三人見江臨沒有進一步解釋的意思,也就識趣地不去多問。
天才總有自己的秘密領域,這點默契他們還是有的。
傍晚,江臨伴著江城的晚霞回到家裡。
吃過晚飯,陪父母聊了會閒話,便回臥室忙自己的事情。
工作站電腦打開,有一封新郵件。
發件人:沈承業。
標題:QF-OLDLIB-001第一批脫敏材料已上傳
正文只有簡短的一句話:江臨,小型私募舊因子庫研究流程審計,正式開始,資源已開通。
江臨點擊連結,開始下載材料。
文件和之前他處理過的那些動輒十幾GB 的Tick級高頻行情數據比起來,不算太大,壓縮包只有不到3個G。
但在量化研究的語境裡,這種體量的信息密度是極其恐怖的。
解壓後,目錄樹展開。
幾張龐大的元數據表。
幾份回測配置摘要。
一個脫敏後的因子輸出矩陣。
一份很粗糙的歷史備註記錄。
還有一張極其關鍵的狀態表。
江臨點開狀態表。表里密密麻麻地列著四百三十七個因子編號。
在狀態這一列里:
有些後面寫著:active(目前仍在實盤交易中提供信號的因子)。
有些寫著:deprecated(因為績效衰減,已被淘汰的因子)。
有些寫著:merged(因為與其他因子相關性過高,被合併的因子)。
有些寫著:deleted(被刪除的因子)。
還有一批,令人觸目驚心地寫著:unknown(未知狀態,連研究員自己都不知道這東西還在不在跑)。
江臨的目光在deleted和unknown這兩個詞上停留了很久,思緒一下子發散開來。
在任何一個量化機構,乃至任何一個科研系統里,成功的東西,總會有人記得。
它們會被寫進PPT,會被掛在年報里,會被用來向投資人吹噓。
但是,失敗的東西,常常最先被刪掉。
而一個沒有失敗記錄的研究系統,最容易把倖存者當成真理。
在金融市場裡,這叫倖存者偏差。
當你只看到那些成功的策略時,你會覺得市場充滿規律。
但如果你看不到那99%因為過度擬合而在實盤中虧得血本無歸的廢棄因子,你就會在下一次研究中,重蹈覆轍。
江臨深吸了一口氣,熟練地打開終端,新建項目文件夾。
然後用Vim打開audit_log.md,敲下了這個審計項目的最高綱領。
第一行:舊因子庫首先不是寶庫,而是失敗記錄的墓地。
第二行:本項目不找神因子,不預測未來,只找「研究流程如何騙過自己」。
第三行:所有結論必須嚴格綁定四個維度:數據版本、樣本池版本、因子版本、回測配置版本。脫離版本談績效,一律視為學術造假。
寫完這三行,他才正式開始第一輪代碼層面的掃描。
最初的審計腳本邏輯並不複雜。
江臨用Python配合Pandas和Dask,寫了幾個守衛器。
唯一性檢查: 檢查因子編號是否唯一,版本號是否連續,每個因子是否嚴格綁定了當時所用的清洗數據版本。
樣本池漂移: 每個回測結果是否綁定了確定的樣本池定義?
失敗記錄完整性: 失敗因子有沒有保留詳細的刪除原因和失效日期的環境切片?
同源性檢查: 同名或近似同名因子是否重複出現?
摩擦成本檢查: 交易成本假設有沒有在不同版本里發生人為的漂移?
回車,運行。
十五分鐘後,第一輪掃描結果出來了。很難看,觸目驚心的難看。
四百三十七個因子裡,有八十多個沒有完整的數據版本號約束。這意味著如果現在重跑回測,根本不知道當初是用什麼數據跑出來的。
二十七個因子版本斷裂,從 v1.2 直接跳到了 v3.0,中間經歷了什麼,無人知曉。
十三個因子名稱完全不同,但備註里的數學邏輯高度相似。
還有幾個因子,名字看起來像是三個截然不同的方向。
但江臨的腳本對它們的脫敏輸出矩陣求了橫截面相關性後發現,它們的皮爾遜相關係數竟然高達0.98。
說明這根本就是同一類想法,在績效壓力和流程失控里被反覆複製、微調參數、改名重跑,試圖碰出一個更好看的夏普比率,最後在歷史裡留下了三具極其相似的影子。
這就是純粹的數據挖掘災難。
江臨面無表情,繼續跑第二輪:因子輸出版本敏感性測試。
第三輪:樣本池時間序列漂移測試。
第四輪:失敗因子記錄完整性穿透。
隨著任務越來越重,工作站腳下的機箱風扇開始持續低鳴,發出沉悶的嗡嗡聲。
江臨瞥了一眼副屏上的系統監控。
溫度:68℃,正常。
硬碟讀寫:正常。
內存占用:正常。
CPU 占用率,卻在幾個特定的腳本執行時,反覆拉高到100%,紅得刺眼。
程序並沒有卡死,終端里的進度條還在走。
但它比江臨預期的慢,慢得非常不合理。
對於現實世界裡的大多數數據科學家或者量化研究員來說,遇到這個情況,第一反應絕對是,機器不夠強。
「老闆,我們要買更好的 CPU,換 AMD 的線程撕裂者。」
「要加更大的內存,把數據全塞進內存里。」
「上AWS雲伺服器,開個 124 核的實例並行跑。」
用硬體的暴力去掩蓋軟體的低效,這是和平年代,資源充沛環境下的通病。
但這不是江臨的第一反應。
在他的腦海深處,那個屬於【廢土世界】的倒計時始終存在。
在資源枯竭的廢土裡,永遠沒有再買一台的選項。
在那裡,機器慢,不能先要資源,必須先問。為什麼慢?是什麼在吃掉算力?
江臨果斷按下了 Ctrl+C,停掉了後續的掃描任務。
打開Python的性能分析工具,將剛才那段跑得極慢的代碼用cProfile 重新包起來,然後接入SnakeViz進行可視化分析。
二十分鐘後,一份詳細的調用棧火焰圖出現在屏幕上。
真正的瓶頸浮出水面。
在調用棧里吞掉最多牆鍾時間的怪物,不是那些幾百萬行的大矩陣乘法。
不是複雜的HDF5文件讀取,不是前端渲染的圖表生成,也不是某個玄學的機器學習複雜模型。
而是幾個小得近乎不起眼的動作:排序、排名、分桶。
在量化回測中,經常需要在特定的行業特定的市值區間內,對股票的因子暴露值進行中性化和排序。
每次參與排序的股票可能不多,只有五個、八個、或者十六個。
單獨看,給五個數排序,不管用什麼算法,每一次都快得像沒有成本,連一毫秒都不需要。
但問題在於乘數效應。
QF-OLDLIB-001里有四百多個因子。
三年歷史版本。
多個動態調整的樣本池。
多種回測配置。
每一天,每一個行業,每一個狀態標記、每一個數據版本,都要切出一層層的橫截面進行分組排名。
於是,這些微小的小動作被嵌套在龐大的循環網裡,被反覆調用。
江臨把調用棧里耗時最高的那個函數次數列印了出來
七千八百四十二萬一千九百零六次。
看著這個天文數字,他沉默了十幾秒,然後在項目的audit_log.md里,他敲下了這樣一段話。
真正拖慢整個複雜系統的,從來都不是偶爾出現的大山,而是每天必須被搬運七千萬次的小石頭。
寫完這句話,他停頓了一下。
為了讓將來可能接手這份審計報告的平庸工程師也能看懂,他又補了一段更通俗的解釋。
一次給五張試卷按分數從高到低排序,對任何人都不難。
難的是,系統要求你一天之內,把給五張試卷排序這個動作,重複七千萬次。
標準庫里的排序算法在計算機科學上被證明是非常優秀的。
但它們優秀的前提是通用。
標準庫就像是一套占地幾萬平方米的大型自動化物流分揀中心。
它可以處理一萬張試卷。
可以處理一百萬個包裹。
可以處理帶有各種奇怪對象的複雜數據結構。
它強大,通用,絕對可靠。
但如果你的流水線上,每次送過來的永遠只有五個小包裹,而且每天要送七千萬次。
那麼,你每一次都去啟動那套耗電巨大的大型物流中心,讓傳送帶空轉,讓機械臂尋址,去執行龐大的分揀邏輯,
這就是不可饒恕的浪費。
在底層代碼的視角里,這種浪費體現為,為了通用性而保留的複雜的函數調用開銷。
為了處理多態而進行的動態類型檢查。
為了兼容不同數組長度,標準流程里保留了大量條件分支。
而這些分支一旦在熱點循環里反覆觸發,就會拖慢現代CPU最依賴的指令流水線。
事實上,並不是系統不會排。
而是流程太重了。
重到CPU的每一個時鐘周期都在被無意義的管理邏輯消耗。
江臨現在要做的,不是去推翻高德納在《電腦程式設計藝術》里寫下的經典排序理論,也不是發明什麼震驚世界的新算法。
他只是需要一套固定手勢。
五張試卷。
看第一張和第二張。
誰大誰在前面,該換就換。
再看第三張和第四張。
該換就換。
幾步極其固定的比較之後,順序自然就出來了。
不問多餘的類型問題。
不打開多餘的內存分配流程。
不為那根本不存在的一百萬張試卷準備任何冗餘的邊界檢查工具。
沒有數據相關的循環,沒有運行時臨時選擇路徑。
比較順序在編譯前就被釘死,剩下的只是固定位置之間的比較與交換。
只處理這五個位置的數字。
這就是在高性能計算領域裡,針對極其明確邊界的小規模數據,進行優化的核心奧義。
凌晨一點二十,萬籟俱寂,江臨在新建的C語言擴展文件里,寫下了第一版函數的簽名。
函數名很醜,甚至不像一個優雅算法庫里的東西。
rank5_fixed_v0。
它不試圖排序世界上一切數組,只處理五個float64因子暴露值,五個有效性標記,以及五個原始位置編號。
輸出的也不是一個漂亮的新數組,而是一組業務排名和一組mask。
它就像一把在廢土車間裡,為了擰某種特定型號引擎底盤上的特定螺絲,而被強行把手柄焊彎的怪異扳手。
但江臨現在需要的,正是這種專一暴力的扳手。
第一版寫完,江臨並沒有急著直接替換到Python 審計主流程里去。
作為在廢土裡見識過一個小數點錯誤導致整個證明功虧一簣的倖存者,他對替換底層邏輯有著病態的嚴謹。
他先做Baseline。
原流程輸出什麼,新函數就必須輸出一模一樣的東西。
在量化金融的數據里,現實永遠比理論骯髒。
無重複值(理想狀態)。
重複值(兩隻股票因子得分完全一樣)。
缺失值(NaN,某隻股票當天停牌沒有數據)。
極端值(Infinity)。
負值。
相等值且需要保持原相對順序(穩定排序要求)。
每一種情況,都必須對齊。
有重複值時,原來在數組裡誰在前面,現在排序後也必須誰在前面。
遇到NaN缺失值時,量化的規則不是數學上的把它當最大或者把它當最小,而是必須按照項目預設的規則,將其單獨剔除並打上MASK_NAN標籤,剩下的數繼續排。
這並非純粹數學定義上的排序,帶著強烈業務屬性的金融審計項目里的排名規則。
兩者絕不能混為一談。
凌晨兩點半,江臨揉了揉發酸的眼睛。
第一版v0跑過了包含兩萬個邊緣測試用例的單元測試。
結果全對。
速度有提升,但並不大,大概只快了15%。
第一版只是為了驗證這個強耦合的方向是走得通的。
江臨並不意外。
他重新打開C 代碼。
開始真正的榨乾性能。
刪掉所有不必要的狀態判斷。
把僅有的一點循環徹底展開,變成直線型的直線代碼。
把可能會出現的各種數據類型的可能性徹底焊死,限定在這個項目里真正會輸入進來的內存布局。
凌晨三點十八,第二版出來了。
他沒有把這個函數暴露成一個給Python循環逐次調用的小玩具。
那樣七千萬次跨語言調用本身就會成為新的災難。
他真正寫的是一個批處理入口。
一次性接收連續內存里的數百萬個五元組,在C層內部跑完整個固定排序網絡,再把排名矩陣吐回給Python。
再次跑測試。
結果一致。
速度提升到了30%。
但這還不夠。
江臨的眉頭微微皺起,他感覺到代碼里還有多餘的脂肪。
從抽屜里拿出一張白紙和一支筆。
紙上,他畫了五個圓圈,標上序號:0,1,2,3,4。
然後開始在圓圈之間連線。
他現在寫的不是代碼,而是動作。
在底層的彙編指令里,比較並交換是一個極其廉價的動作。
只要沒有if分支造成的預測失敗,指令就可以像水一樣順暢地通過 CPU流水線。
比較0和1(大的去右邊)。
比較3和4。
比較2和4。
比較2和3。
比較1和4。
比較0和3。
……
每一步,都像是在進行一次精密的機械手工調整。
五個數排好序,根本不需要程序在每一次運行的時候去思考接下來該怎麼辦。
路線是可以提前定死的。
就像水流經過預先挖好的迷宮溝渠,無論水勢大小,最終都會從既定的出口按照大小順序流出。
只要這套網格路線上,所有可能的 $5! = 120$ 種初始排列,最後都能被正確地疏導成有序狀態,就足夠了。
這就是計算機科學中極其冷門但極其硬核的概念。
排序網絡。
它一點也不聰明。
面對1000個數它毫無辦法。
但它非常穩定。
它不通用,但它是為高頻,小規模任務量身定製的終極殺器。
寫到這裡,江臨停下了筆。
忽然想起了上午在B304里,那個放在桌角的磁性幾何魔方,想起了那道阿里數學競賽的盲盒題。
他記得自己對尹航說過的話,先別被圖案牽著走,先找上界,再找取等構造。
優化排序底層的邏輯也一樣。
先別被排序這個在教科書里被講爛了的大詞嚇住。
他現在要處理的,根本不是排序學,只是五個內存位置之間有限次比較交換組合的最小充分集。
這是一個很小的世界。
小到它的所有狀態都可以被數學窮盡。
小到它是可以被嚴格證明的。
但它重要到,只要這個動作被重複七千萬次,它就會變成拖垮龐大金融系統的結構性瓶頸。
凌晨四點十分,窗外的天際已經隱隱有了一絲青灰色。
江臨敲下最後一組指令,第三版完成。