第90章 七千万次的小石头

      白板前,尹航还在盯著那道拆盲盒题。
    他的大脑正在高速运转,试图在这张错综复杂的概率网络里找到哪怕一丝破绽。
    姚思雨站在另一侧,手里握著板擦,已经把旧递推公式毫不留情地划掉。
    江临站在一旁,却没有继续往下讲。
    这几道题到这里就已经足够了。
    阿里全球数学竞赛预赛的题目,真正的价值从来不在於他把最终那个光禿禿的答案餵给別人,而在於看到那条线的过程。
    先抽象。
    把现实世界里极其复杂的盲盒概率、卡池保底机制,剥离掉所有花哨的商业外衣,变成纯粹的马尔可夫链。
    再压缩。
    把数以万计的节点状態,根据对称性压缩成一个只与剩余未收集种类数相关的最小一维数组。
    最后验证。
    用严密的逻辑去证明,这种降维压缩没有在任何一个微小的概率分岔口上丟掉必要信息。
    尹航手里的马克笔在指尖转了半圈,笔帽啪地一声敲在掌心。
    他转过头,用一种近乎看怪物的眼神看著江临,忽然问:“你別告诉我,这十一道题,全都是像刚才这样,一眼看穿底层结构,然后没有任何卡顿地写完的?”
    江临想了想,认真地摇了摇头:“不全是,有一道代数变形题更偏纯粹的计算和排版表达,没有这么强的结构感,纯粹靠算力堆砌。而且,我也试了几条错路。”
    尹航听到错路两个字,肩膀猛地塌了下来,似乎长长地鬆了口气:“靠,原来你也会走错路啊,我还以为你脑子里自带量子计算机呢。”
    “我走的错路可能比你们任何人都多。”
    江临说著,还举例说明將自己当初在一道数论题上被复杂的表象迷惑,走进死胡同的过程说了一遍。
    “所以,你是找正確路线,排除错误方向,大概花了四十分钟。后面用latex整理提交版排版,花了一个多小时。”
    尹航被江临这番一本正经的诉苦气笑了。
    “谢谢你啊,你终於承认自己花了时间,终於像个人了。但我光是找这道盲盒题的路线,花的时间就是你的四倍,而且还没找全。”
    屋里的气氛终於彻底鬆弛了下来。
    那种因为高强度智力对抗而紧绷的空气,隨著尹航的自嘲烟消云散。
    姚思雨也忍不住笑了一下:“所以陆导让你把b304当成自己家常来,不是没有原因的。你在这里,至少能让我们知道,题目还能被拆到什么程度,不至於刚摸到门槛就以为自己看见了天花板。”
    边上的孟澈见江临开始收拾东西,知道他要回去,於是把下载好的几份数据说明与论文列印稿递过去,隨口问了一句:“最近除了阿里预赛,还在忙什么,感觉你这个月都是神出鬼没的。”
    “写一个数据检查工具。”
    “做什么的?”
    “量化相关吧。”
    “你还搞量化?”孟澈讶然道。
    他正在研究机器学习,很清楚量化金融领域的水有多深。
    “你才多大啊,你就搞量化?”尹航也是一副难以置信的模样,“你別告诉我你已经开始炒股了。”
    “也就是隨便看看,帮人做点基础的数据梳理。”
    但b304三人组显然不相信。
    一个能把阿里预赛十一道题当切菜一样做完的人,嘴里的隨便看看,绝对不可能是用excel画几张k线图那么简单。
    不过三人见江临没有进一步解释的意思,也就识趣地不去多问。
    天才总有自己的秘密领域,这点默契他们还是有的。
    傍晚,江临伴著江城的晚霞回到家里。
    吃过晚饭,陪父母聊了会閒话,便回臥室忙自己的事情。
    工作站电脑打开,有一封新邮件。
    发件人:沈承业。
    標题:qf-oldlib-001第一批脱敏材料已上传
    正文只有简短的一句话:江临,小型私募旧因子库研究流程审计,正式开始,资源已开通。
    江临点击连结,开始下载材料。
    文件和之前他处理过的那些动輒十几gb 的tick级高频行情数据比起来,不算太大,压缩包只有不到3个g。
    但在量化研究的语境里,这种体量的信息密度是极其恐怖的。
    解压后,目录树展开。
    几张庞大的元数据表。
    几份回测配置摘要。
    一个脱敏后的因子输出矩阵。
    一份很粗糙的歷史备註记录。
    还有一张极其关键的状態表。
    江临点开状態表。表里密密麻麻地列著四百三十七个因子编號。
    在状態这一列里:
    有些后面写著:active(目前仍在实盘交易中提供信號的因子)。
    有些写著:deprecated(因为绩效衰减,已被淘汰的因子)。
    有些写著:merged(因为与其他因子相关性过高,被合併的因子)。
    有些写著:deleted(被刪除的因子)。
    还有一批,令人触目惊心地写著:unknown(未知状態,连研究员自己都不知道这东西还在不在跑)。
    江临的目光在deleted和unknown这两个词上停留了很久,思绪一下子发散开来。
    在任何一个量化机构,乃至任何一个科研系统里,成功的东西,总会有人记得。
    它们会被写进ppt,会被掛在年报里,会被用来向投资人吹嘘。
    但是,失败的东西,常常最先被刪掉。
    研究员为了掩盖自己几个月毫无建树的尷尬,或者为了让代码库看起来乾净,会毫不犹豫地按下delete键。
    而一个没有失败记录的研究系统,最容易把倖存者当成真理。
    在金融市场里,这叫倖存者偏差。
    当你只看到那些成功的策略时,你会觉得市场充满规律。
    但如果你看不到那99%因为过度擬合而在实盘中亏得血本无归的废弃因子,你就会在下一次研究中,重蹈覆辙。
    江临深吸了一口气,熟练地打开终端,新建项目文件夹。
    然后用vim打开audit_log.md,敲下了这个审计项目的最高纲领。
    第一行:旧因子库首先不是宝库,而是失败记录的墓地。
    第二行:本项目不找神因子,不预测未来,只找“研究流程如何骗过自己”。
    第三行:所有结论必须严格绑定四个维度:数据版本、样本池版本、因子版本、回测配置版本。脱离版本谈绩效,一律视为学术造假。
    写完这三行,他才正式开始第一轮代码层面的扫描。
    最初的审计脚本逻辑並不复杂。
    江临用python配合pandas和dask,写了几个守卫器。
    唯一性检查: 检查因子编號是否唯一,版本號是否连续,每个因子是否严格绑定了当时所用的清洗数据版本。
    样本池漂移: 每个回测结果是否绑定了確定的样本池定义?
    失败记录完整性: 失败因子有没有保留详细的刪除原因和失效日期的环境切片?
    同源性检查: 同名或近似同名因子是否重复出现?
    摩擦成本检查: 交易成本假设有没有在不同版本里发生人为的漂移?
    回车,运行。
    十五分钟后,第一轮扫描结果出来了。很难看,触目惊心的难看。
    四百三十七个因子里,有八十多个没有完整的数据版本號约束。这意味著如果现在重跑回测,根本不知道当初是用什么数据跑出来的。
    二十七个因子版本断裂,从 v1.2 直接跳到了 v3.0,中间经歷了什么,无人知晓。
    十三个因子名称完全不同,但备註里的数学逻辑高度相似。
    还有几个因子,名字看起来像是三个截然不同的方向。
    但江临的脚本对它们的脱敏输出矩阵求了横截面相关性后发现,它们的皮尔逊相关係数竟然高达0.98。
    说明这根本就是同一类想法,在绩效压力和流程失控里被反覆复製、微调参数、改名重跑,试图碰出一个更好看的夏普比率,最后在歷史里留下了三具极其相似的影子。
    这就是纯粹的数据挖掘灾难。
    江临面无表情,继续跑第二轮:因子输出版本敏感性测试。
    第三轮:样本池时间序列漂移测试。
    第四轮:失败因子记录完整性穿透。
    隨著任务越来越重,工作站脚下的机箱风扇开始持续低鸣,发出沉闷的嗡嗡声。
    江临瞥了一眼副屏上的系统监控。
    温度:68c,正常。
    硬碟读写:正常。
    內存占用:正常。
    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里,那个放在桌角的磁性几何魔方,想起了那道阿里数学竞赛的盲盒题。
    他记得自己对尹航说过的话,先別被图案牵著走,先找上界,再找取等构造。
    优化排序底层的逻辑也一样。
    先別被排序这个在教科书里被讲烂了的大词嚇住。
    他现在要处理的,根本不是排序学,只是五个內存位置之间有限次比较交换组合的最小充分集。
    这是一个很小的世界。
    小到它的所有状態都可以被数学穷尽。
    小到它是可以被严格证明的。
    但它重要到,只要这个动作被重复七千万次,它就会变成拖垮庞大金融系统的结构性瓶颈。
    凌晨四点十分,窗外的天际已经隱隱有了一丝青灰色。
    江临敲下最后一组指令,第三版完成。