机器学习、云计算、云原生等技术的进步给金融行业创新注入了新的动力,以乾象投资 Metabit Trading 为代表的以人工智能为核心的科技型量化投资公司的工作就非常有代表性。他们通过深度融合和改进机器学习算法,并将其应用于信噪比极低的金融数据中,为投资人创造长期可持续的回报。
与传统的量化分析不同,机器学习不仅关注股价、交易量和历史回报率等结构化数据,还注入来自研报、财报、新闻和社交媒体等非结构化数据来深入了解证券价格走势和波动性。然而,将机器学习应用于量化研究是具有挑战性的,因为原始数据可能包含噪声。此外,他们还需要应对许多挑战,如突发任务、高并发数据访问和计算资源限制等。
为此,乾象投资在研发投入、创新支持和基础平台建设方面持续发力。他们的研究基础设施团队构建了一个高效、安全、规模化的工具链研发流程,通过合理利用云计算和开源技术突破了单机研发的限制。本文将分享乾象量化研究基础平台的具体实践,介绍基于 Fluid+JuiceFSRuntime 的公共云弹性量化投研工作支撑。
量化研究的工作详解
作为 AI-powered hedge fund,通过 AI 模型训练进行策略研究是我们最主要的研究方式。首先,在模型训练之前需要对原始数据做特征提取。金融数据的信噪比特别低,如果直接使用原始的数据进行训练,得到的模型噪音会非常大。原始数据除了行情数据,即大家经常会看到的市场上的股价、交易量之类的数据,也包括一些非量价的数据,比如研报、财报、新闻、社交媒体等之类的非结构化数据,研究人员会通过一系列的变换提取出特征,再进行 AI 模型训练。可以参考下面我们研究场景中和机器学习关联最紧密的策略研究模式的简化示意图。
模型训练会产出模型以及信号。信号是对未来价格趋势的判断,信号的强度意味着策略导向性的强度。量化研究员会根据这些信息去优化投资组合,从而形成交易的实时仓位。这个过程中会考虑横向维度(股票)的信息来进行风险控制,例如某一行业的股票不要过度持仓。当仓位策略形成之后,量化研究员会去模拟下单,而后得到实时仓位对应的盈亏信息,从而了解到这个策略的收益表现,这就是一个量化研究的完整流程。02
量化研究基础平台的需求
第一,突发任务多,弹性要求高。在策略研究的过程中,量化研究员会产生策略想法,并会通过实验去验证自己的想法。伴随着研究人员新想法的出现,计算平台就会产生大量的突发任务,因此我们对计算的弹性伸缩能力要求很高。
上图是我们某个集群一段时间的运行实例数据。以上图为例,可以看到在多个时间段里,整个集群实例数高峰时刻可以达到上千个,但是同时整个计算集群的规模也会有缩容到 0 时候。量化机构的计算任务和研究员的研发进度是有很大关联的,波峰波谷的差距会非常大,这也是离线研究任务的特点。
第二,热数据高并发访问,除了计算需要弹性,数据缓存也需要弹性。对于热数据,比如行情数据,通常会有上百个任务同时访问数据,它的吞吐要求非常高,峰值时数百 Gbps 甚至 Tbps 级别的聚合带宽才能满足需求。但是当计算集群中没有任何节点的时候,此时的吞吐需求为 0,如果是刚性吞吐这就需要弹性吞吐扩缩容的能力。
第三,容量和吞吐的独立线性扩展能力,对金融模型训练非常重要。传统分布式存储带宽与吞吐仅和数据使用容量成正比,而量化研究过程中会创建大量的容器并发访问存储系统的数据,会触发存储系统访问限流。这就造成计算资源极致弹性与存储系统有限带宽之间的矛盾。而量化研究的数据量其实不是特别大,很多市场的量价数据总量也不会超过 TB 级,但是数据访问需要的峰值吞吐却非常高。
第四,数据亲和性调度,同一数据源多次运行访问本地缓存可以被复用。充分发挥热点数据集的缓存节点优势,在对用户无感知的前提下,智能的将任务调度到数据缓存节点上。让常用的模型训练程序越来越快。
第五,IP保护:数据共享与数据隔离。出于 IP 保护的需求,不仅在计算任务上需要做隔离,在数据上也是需要具备权限控制的隔离能力;同时对行情数据这类相对公开的数据,还需要支持研究员的获取方式是便捷的。
第六,缓存中间结果。计算任务模块化的场景会对中间结果的存储跟传输也有需求。举个简单的例子,在特征计算过程中会生成比较大量的特征数据,这些数据会立刻用于接下来大规模高并发的训练节点上。显而易见在这种场景下我们需要一个高吞吐和高稳定的中间缓存做数据传递。
第七,多文件系统的支持。计算任务中各类型的任务会对应的各种特性的数据类型和使用方式,因而我们不同团队会采用不同的文件系统包括 OSS,CPFS,NAS,JuiceFS,以获取在各自情况下的性能最优化。Fluid 的不同 runtime 能够灵活的支持文件系统与任务的组合,使得任务计算能够在 K8s 上更高效合理的利用对应资源避免不必要的浪费。03
Fluid+JuiceFSRuntime:为云上量化研究基础平台提供高效支撑
出于 POSIX 兼容,成本,高吞吐的考虑,我们选择了 JuiceFS 云服务作为分布式底层存储。选择了 JuiceFS,发现现有 Kubernetes 的 CSI 体系并不能很好地支持我们对数据访问性能、弹性吞吐能力以及数据共享隔离的需求,具体来看:
1. 传统的 Persistent Volume Claim 是面向通用存储的抽象,缺乏对同一个存储复杂数据访问模式协同良好的支持:在不同的应用场景下,应用对同一存储中不同文件的使用方式不同,比如我们多数并发计算任务要求只读;但是也有 Pipeline 数据中转,数据特征生成之后,需要中转到模型训练中,此时就要求读写;这导致了很难在同一个 PVC 中统一设置元数据更新和缓存策略。实际上,这些策略应该完全取决于应用使用数据的模式。
2. 数据隔离与共享:不同数据科学家团队访问不同的数据集需要天然隔离,并且要求比较容易管理;同时支持公共数据集访问共享,特别是缓存数据共享,由于行情数据这类相对公开的数据,不同的研究员团队会反复使用,希望获得“一次预热、全公司收益”的效果。
3. 数据缓存感知的 Kubernetes 调度:相同模型、相同输入、不同的超参的作业以及微调模型、相同输入的作业都会不断重复访问同一数据,产生可以复用的数据缓存。但是原生的 Kubernetes 调度器无法感知缓存,导致应用调度的结果不佳、缓存无法重用,性能得不到提升。
4. 数据访问吞吐可以弹性扩容到数百 Gbps:传统的高性能分布式文件存储,一般的规格是 200 MB/s/TiB 基线的存储规格,其最大 IO 带宽是 20Gbps,而我们任务的峰值 IO 带宽需求至少需要数百 Gbps,显然无法满足我们的要求。
5. 数据缓存的成本最优:由于公共云提供了计算资源极致弹性,可以短时间内弹出几百甚至上千计算实例,而当这些计算资源同时访问存储时,在高峰时吞吐需要数百 Gbps 甚至更高,此时需要通过计算中的缓存集群去服务热数据。但是很多时间段内,计算集群会缩容为 0,此时维护一个很大的缓存集群就得不偿失了。我们更倾向于在使用之前进行数据预热,同时根据业务的运行规律执行定时扩缩容;而当计算集群没有作业在运行,再缩容到默认缓存节点,从而达到对数据缓存吞吐的动态弹性伸缩控制。
为了达到上述目标,我们迫切希望找到 Kubernetes 上具有弹性分布式缓存加速能力同时很好支持 JuiceFS 存储的软件。我们发现 CNCF Sandbox 项目 Fluid[1] 和 JuiceFS 存储有很好的协同,JuiceFS 团队正好也是 Fluid 项目中 JuiceFSRuntime 的主要贡献者和维护者。于是,我们设计了基于 Fluid 的架构方案并选择了原生的 JuiceFSRuntime。
架构组件介绍
Fluid
Fluid 不同于传统的面向存储的 PVC 抽象方式,而是在 Kubernetes 上针对“计算任务使用数据”的过程进行抽象。它提出了弹性数据集 Dataset 的概念,以应用对数据访问的需求为中心,给数据赋予特征,如小文件、只读、可读写等;同时将数据从存储中提取出来,并且给有特征的数据赋予范围,如用户只关心某几天的数据。围绕 Dataset 构建调度系统,关注数据本身和使用数据的应用的编排,强调弹性和生命周期管理。
JuiceFSRuntime
JuiceFSRuntime 基于 JuiceFS 的分布式缓存加速引擎, 通过将数据分布式缓存技术与Fluid自动弹性伸缩(Autoscaling)、可迁移(Portability)、可观测(Observability)、亲和性调度(Scheduling)能力相结合,支持场景化的数据缓存和加速能力。在Fluid上使用和部署 JuiceFSRuntime 流程简单、兼容原生 Kubernetes 环境、可以开箱即用,并深度结合 JuiceFS 存储特性,针对特定场景优化数据访问性能。
使用基于 JuiceFSRuntime 的 Fluid 的原因
1. Dataset 抽象满足云原生机器学习场景的性能优化和隔离共享等多样需求:
- 场景化性能调优:通过 Dataset 可以针对不同访问特点的数据集作相应的优化,比如模型训练场景通常是只读,而特征计算需要读写。
- 数据隔离:Dataset 天然的通过 Kubernetes 的命名空间这种资源隔离机制用来限制不同团队对集群中数据集的访问权限,并且不同的数据集对应 JuiceFS 中不同的子目录(JuiceFS 企业版还支持目录配额),这可以满足数据隔离的需求。
- 数据缓存共享:对于一些不同团队都会频繁使用的公开数据集,Fluid 支持跨Kubernetes Namespace 的数据访问,可以做到一次缓存,多个团队共享,这也满足了数据缓存共享的需求。
2. Runtime 场景化的计算资源优化:Dataset 是数据的通用抽象,而对于数据真正的操作,实际上由 JuiceFSRuntime 实现,所以 Runtime 的 CPU,Memory,网络和缓存 Worker 数量等资源配置势必会影响性能,这就需要针对 Dataset 的使用场景,对 Runtime 的计算资源进行优化配置。
3. 弹性分布式缓存:支持丰富的扩缩容策略,包括手动伸缩、自动弹性伸缩和定时弹性伸缩,可以根据需要找到最优的弹性方案。
- 手动伸缩:通过 Dataset 的可观测性,可以了解数据集的数据量和缓存 Runtime 需要的缓存资源,也可以根据应用访问的并发度设置 Runtime 的 Replicas 数量(缓存 Worker 的数量),不用的时候可以随时缩容。
- 自动弹性伸缩:根据数据指标进行自动弹性伸缩。比如根据数据缓存量和吞吐量等指标进行缓存弹性伸缩,可以制定当缓存百分比超过 80%,或者当客户端数量超过一定阈值的时候进行自动扩容。
- 定时弹性伸缩:根据业务特性设置定时弹性伸缩,可以实现无人参与的数据弹性扩缩容机制。
4. 自动的数据预热:避免在训练时刻并发拉取数据造成数据访问竞争,还可以和弹性伸缩配合,避免过早的创建缓存资源。
5. 数据感知调度能力:在应用被调度时,Fluid 会通过 JuiceFSRuntime 把数据缓存位置作为一个调度信息提供给 K8s 调度器,帮助应用调度到缓存节点或者离缓存更近的节点。整个过程对用户透明,实现数据访问性能的优势最大化。
落地实践
根据实践,我们总结了以下经验供大家参考。
在 Fluid 的 JuiceFSRuntime 选择高网络 IO 和大内存的 ECS 作为缓存节点
随着 ECS 网络能力的不断提升,当前网络带宽已经远超 SSD 云盘 IO 能力。以阿里云上的 ecs.g7.8xlarge 规格的 ECS 为例,其带宽峰值为 25Gbps,内存为 128GiB。理论上,完成 40G 数据读取仅需要 13s。我们的数据是存储在 JuiceFS 上的,因此为了实现大规模的数据读取,我们需要首先将数据加载到计算所在 VPC 网络中的计算节点中。以下为我们使用的一个具体例子,为了提高数据读取速度,我们配置 cache 节点使其选择使用内存来缓存数据。这里需要注意:
- Worker 主要负责分布式数据缓存,为了提高数据读取速度,我们可以为 Worker 配置内存性能相对较高的机型。而 Worker 的调度策略在 Dataset 中配置,因而需要在 Dataset 中为 Worker 配置亲和性策略。
- 当任务没有特定机型需求时,为保证 Cluster 的 AutoScaler 能成功扩容,实践中也建议在进行亲和性配置时选择多种实例类型以保证扩容/任务执行的成功。
- Replicas 是初始的缓存 Worker 数量,后期可以通过手动触发或自动弹性伸缩进行扩缩容。
- 当指定 tieredstore 后,即无需在 Kubernetes 的 Pod 中设置 request 的内存,Fluid 可以自动处理。
- 如在缓存节点上的 JuiceFS mount 有不同的配置,例如 cache size 大小, 挂载路径等,可以通过 worker 里的 options 进行覆盖。
apiVersion: data.fluid.io/v1alpha1 kind: Dataset metadata: name: metabit-juice-research spec: mounts: - name: metabit-juice-research mountPoint: juicefs:/// options: metacache: "" cache-group: "research-groups" encryptOptions: - name: token valueFrom: secretKeyRef: name: juicefs-secret key: token - name: access-key valueFrom: secretKeyRef: name: juicefs-secret key: access-key - name: secret-key valueFrom: secretKeyRef: name: juicefs-secret key: secret-key nodeAffinity: required: nodeSelectorTerms: - matchExpressions: - key: node.kubernetes.io/instance-type operator: In values: - ecs.g7.8xlarge - ecs.g7.16xlarge tolerations: -key: jfs_transmittion operator: Exists effect: NoSchedule --- apiVersion: data.fluid.io/v1alpha1 kind: JuiceFSRuntime metadata: name: metabit-juice-research spec: replicas: 5 tieredstore: levels: - mediumtype: MEM path: /dev/shm quota: 40960 low: "0.1" worker: nodeSelector: nodeType: cacheNode options: cache-size: 409600 free-space-ratio: "0.15“
配置自动弹性伸缩策略
受业务形态的影响,Metabit 在固定时段会有跟高的用量需求,因此简单的配置定时缓存节点的弹性伸缩策略能到达到不错的收益,例如对成本的控制,对性能提升。
apiVersion:
autoscaling.alibabacloud.com/v1beta1 kind: CronHorizontalPodAutoscaler metadata: name: research-weekly namespace: default spec: scaleTargetRef: apiVersion: data.fluid.io/v1alpha1 kind: JuiceFSRuntime name: metabit-juice-research jobs: - name: "scale-down" schedule: "0 0 7 ? * 1" targetSize: 10 - name: "scale-up" schedule: "0 0 18 ? * 5-6" targetSize: 20
更进一步,如果通过业务中具体的 metrics 如缓存比例阈值,IO throughput 等触发带有复杂自定义规则的弹性伸缩策略可以实现更为灵活的缓存节点扩缩容配置以带来更高和更稳定的性能表现。具体来讲,在灵活度和性能层面会有以下一些优点:
- 无需精准感知底层数据或拥有固定的扩缩容规则, 依据集群状态自适应的配置缓存副本上下限。
- 阶梯式扩容避免一开始就创建过多的 ECS,造成花费上的浪费。
- 避免爆发式的 ECS 访问 JuiceFS 数据造成带宽抢占。
触发数据预热
通过数据预热提升缓存比例,进而触发自动弹性伸缩;同时监控缓存比例,当缓存比例达到一定阈值同时开始触发任务下发,避免过早下发高并发任务导致 IO 延迟。
镜像预埋
由于 Metabit Trading 使用计算和数据弹性的规模很大,瞬间会弹出非常多的 Pod,这就会导致镜像下载限流。网络带宽资源在 pod 拉起过程中是稀缺的,为避免pod 创建时因拉取容器镜像延时带来的各种问题,我们建议对 ECS 的镜像进行客制化改造,对需要的系统性镜像做到“应埋尽埋”从而降低 pod 拉起的时间成本。具体例子可参考 ACK[2] 的基础镜像。04
弹性吞吐提升带来的性能和成本收益
在实际部署评估中,我们使用 20 个 ecs.g7.8xlarge 规格的 ECS 作为 woker 结点构建 JuiceFSRuntime 集群,单个 ECS 结点的带宽上限为 25Gbps;为了提高数据读取速度,我们选择使用内存来缓存数据。
为便于对比,我们统计了访问耗时数据,并与使用 Fluid 方式访问数据耗时进行了对比,数据如下图所示:
可以看出,当同时启动的 Pod 数量较少时,Fluid 与分布式存储相比并没有明显优势;而当同时启动的 Pod 越多时,Fluid 的加速优势也越来越大;当同时并发扩大到 100 个 Pod 时,Fluid相比传统分布式存乎可以降低超过 40% 的平均耗时。这一方面提升了任务速度,另外一方面也确实的节省了 ECS 因为 IO 延时带来的成本。
更重要的是,因整个 Fluid 系统的数据读取带宽是与 JuiceFSRuntime 集群规模正相关的,如果我们需要同时扩容更多的 Pod,则可以通过修改 JuiceFSRuntime 的 Replicas 来增加数据带宽,这种动态扩容的能力是分布式存储目前无法满足的。05
展望
Metabit 在 Fluid 的实践上走出了踏实的第一步,面对这个不断创新和持续输出的技术框架我们也在思考如何发挥在更多合适的场景发挥其完备的功能。这里简单聊聊我们的一些小观察,抛砖引玉,供大家发挥。
1. Serverless 化能够提供更好的弹性:目前我们通过自定义镜像的方式提升应用容器和Fluid组件的弹性效率,我们也关注到在 ECI 上使用 Fluid 能更高效和简单的应用扩展弹性,同时降低运维复杂度。这是一个在实践中值得去探索的方向。
2. 任务弹性和数据缓存弹性的协同:业务系统了解一段时间内使用相同数据集的任务并发量,并且任务排队的过程中执行数据预热和弹性扩缩容;相应的当数据缓存或者数据访问吞吐满足一定条件,触发排队任务从等待变成可用。06
总结和致谢
Metabit Trading 在生产环境使用 Fluid 已经接近一年半了,包括 JindoRuntime、JuiceFSRuntime 等,目前通过 JuiceFSRuntime 实现了高效的大规模量化研究。Fluid 很好的满足了简单易用、稳定可靠、多 Runtime、易维护以及让量化研究员的使用感透明等好处。
Metabit Trading 的大规模实践帮助我们团队在使用公共云上积累了很好的认知,在机器学习和大数据场景下,不但计算资源需要弹性,与之配合的数据访问吞吐也需要与之相匹配的弹性,传统的存储侧缓存由于成本,灵活,按需弹性的差异,已经很难满足当前场景的需求,而 Fluid 的计算側弹性数据缓存的理念和实现则非常合适。
这里要特别感谢 JuiceData 的朱唯唯,Fluid 社区的车漾,徐之浩和顾荣老师的持续支持。因为有他们的维护,社区内有活跃的讨论和快速的响应,这对我们的顺利 adoption 起到了关键的作用。07
相关链接
[1] Fiuld
https://github.com/fluid-cloudnative/fluid
[2] ACK
https://www.aliyun.com/product/kubernetes?spm=5176.19720258.J_3207526240.33.1d2276f46jy6k6
08
作者介绍
李治昳,Metabit Trading - AI Platform Engineer,builder 、云原生技术 learner,曾任Citadel高级工程师。
李健弘, Metabit Trading - Engineering manager of AI Platform,专注于在量化研究领域搭建机器学习平台和高性能计算平台,曾任 Facebook 高级工程师。
本文作者来自乾象投资 Metabit Trading,公司成立于2018年,是一家以人工智能为核心的科技型量化投资公司。核心成员毕业于 Stanford、CMU、清北等知名高校。目前,管理规模已突破 50 亿元人民币, 并且在2022年市场中性策略收益排名中名列前茅,表现亮眼。
本文转载来源:infoq
如果你对 Fluid 项目感兴趣,欢迎点击此处了解更多