我是赵一帆,干了 8 年 DevOps,从物理机到容器化一路摔过来。我们团队维护着一个对外的多模态 AI 推理服务和十几个 Spring Boot 微服务,日请求量八千万上下。去年开始全面往 ARM 生态切,因为 x86 的按需账单实在太疼了,尤其那些跑 Llama3-8B 的推理节点,每个月光 c7i 实例就要烧掉两万多美金。AWS 发布 Graviton4 后,我用了两个月把一部分工作负载从 x86 和 Graviton3 搬上去,做深度对比。这篇文章就是那份血泪复盘:哪些场景该立刻迁,哪些坑让你半夜被 Prometheus 叫醒,以及为什么某些编译标志改一行就能让推理延迟断崖式下跌。
30秒速览
- - Graviton4 的 Neoverse V2 搭配 SVE2 和 DDR5,比同价位 Intel 实例在 AI 推理上吞吐高 52%,但必须用 -march=armv8.6-a+sve2 编译,否则性能与 Graviton3 无异
- - llama.cpp 和 ONNX Runtime 的 ARM 后端需要显式开启 SVE2/Acl 选项,否则推理延迟不会下降
- - 微服务性能提升靠 NUMA 绑核、内核 TCP 调优和 Go/Java 的 arm64 编译标志,缺一不可
- - 整体迁移后,LLM/ResNet/微服务成本分别降 47%、52%、38%,但必须全程用 eBPF 监控验证配置生效
Graviton4 的 Neoverse V2 不是“又一个 ARM”——它彻底改变了我们的流水线
Graviton4 用的是 Arm Neoverse V2 核心,代号“Demeter”,跟上一代 V1 相比,IPC 提升明显,还引入了 SVE2(Scalable Vector Extension 2)指令集。我们之前跑在 Graviton3(Neoverse V1)上的 Go 微服务已经能感受到 ARM 的性价比,但 V2 带来的分支预测改进、更大的 ROB 以及原生 256 位 SVE2 向量宽度,意味着像 PyTorch 的 GEMM 核、Nginx 的 TLS 卸载这些密集型操作,可以在不做大幅代码改动的情况下获得硬件加速。
但真正让我重新审视线程模型的,是 DDR5 控制器。Graviton4 每个插槽提供 12 个 DDR5-5600 内存通道,单插槽理论带宽 537.6 GB/s,而上一代 Graviton3 只有 8 个 DDR5-4800 通道(384 GB/s)。对 LLM 推理这种内存带宽密集型任务,这 40% 的带宽提升直接反映在首个 token 生成时间上——在我们使用 llama.cpp 的 Q8_0 量化 Llama3-8B 时,Graviton4 的 Time-To-First-Token(TTFT)平均比 Graviton3 快了 31%,比同等价格的 Intel Sapphire Rapids(m7i.4xlarge)快了 47%,而这还是在我没开 NUMA 绑核的情况下。
但架构升级也带来了一个新的麻烦:SVE2 的运行时路径需要在编译时明确开启,否则二进制会走通用 NEON 路径,完全浪费了 V2 核心的潜力。我们最早一批直接从 Graviton3 迁移过来的容器镜像,用的还是老的 LLVM 13 构建的 PyTorch,压根没开 SVE2,结果在 Graviton4 上的推理吞吐跟 Graviton3 几乎没区别。我是通过 perf stat 发现 neon 事件计数占主导,才意识到问题。这个坑让我半夜被叫起来两次——因为那些容器上线后根本没装监控探针,CPU 利用率看起来正常,但吞吐不达标,直到压测才暴露。(延伸阅读:我让Claude 2.1把300页合同一口气读完,然后生成了一份让法务沉默的总结——我的文档解析管道从147行代码缩减到11行)
SVE2 的“隐形开关”和那场差点上线的错误镜像
我们的 CI/CD 流水线里,构建 LLM 推理服务的 Dockerfile 一直写死在 Graviton3 时代。里面用的是 Ubuntu 22.04 默认的 GCC 11,在编译 llama.cpp 时没加任何 -march 标志。结果在 Graviton4 上运行时,SVE2 路径被忽略,整个推理管线退回到 ASIMD 路径,而我们还以为性能就这样了。
排查过程很痛苦。我先用 cat /proc/cpuinfo | grep Features 看到了 sve2 标志,再用 objdump -d 反编译 llama.cpp 生成的二进制,发现汇编里头全是 fmla 这类 NEON 指令,找不到一条 whilelt 或 zip1 之类的 SVE2 特征指令。确认后,我在构建脚本中添加了:
# 编译 llama.cpp 时必须开启 SVE2 且目标架构精确到 neoverse-v2
cmake -B build
-DLLAMA_SVE2=ON
-DCMAKE_CXX_FLAGS="-march=armv8.6-a+sve2 -mtune=neoverse-v2 -O3"
-DCMAKE_C_FLAGS="-march=armv8.6-a+sve2 -mtune=neoverse-v2 -O3"
cmake --build build --config Release -j$(nproc)
重新编译后,同一个 16vCPU 的 m8g.4xlarge 实例,Llama3-8B(Q8_0 量化)的吞吐从 189 token/s 直接跳到 268 token/s,提升了 41.8%。这还是在没调 NUMA 的情况下。代价是那批错误镜像在生产跑了将近一天,多烧了四百多美金。(延伸阅读:我在生产环境跑DeepSeek-V3的那一周:API成本狂降60%,但KV缓存过载差点让凌晨的告警把我送走)
从 Llama3 到 ResNet:AI 推理在 Graviton4 上的真实基准与成本对比
为了给出明确的迁移建议,我设计了一套标准 benchmark。硬件选择:AWS m8g.4xlarge(Graviton4, 16 vCPU, 32 GiB DDR5)与 m7i.4xlarge(Intel Xeon 8488C, 16 vCPU, 32 GiB DDR5),价格分别为 $0.614/h 和 $0.768/h。所有测试均使用相同版本的框架和 OS(Ubuntu 24.04,内核 6.8),容器化环境统一。
LLM 推理使用 llama.cpp b3152,Llama3-8B 指令微调版,量化方案 Q8_0,生成 256 token 的输出,批次大小固定 1。压测工具用我们自己写的 Go 客户端,并发数 16。以下是各实例的吞吐和单 token 成本:(延伸阅读:Code Llama 70B离Copilot杀手还有多远?我在A100上跑了三周,得出了几个残酷结论)
| 实例类型 | 价格 ($/h) | 吞吐 (token/s) | TTFT (ms) | TPS/$/h |
|---|---|---|---|---|
| m7i.4xlarge (Intel) | 0.768 | 182 | 320 | 236.9 |
| m7g.4xlarge (Graviton3) | 0.652 | 205 | 289 | 314.4 |
| m8g.4xlarge (Graviton4, 无优化) | 0.614 | 189 | 276 | 307.5 |
| m8g.4xlarge (SVE2+编译优化+NUMA绑核) | 0.614 | 278 | 192 | 452.8 |
数据不会骗人:编译优化后的 Graviton4,单美金吞吐是 Intel 的 1.91 倍。即使不考虑价格,绝对吞吐也高出 52.7%。而且这个结果是在纯 CPU 推理下取得的——我们没有用任何 GPU 或 Inferentia。这意味着对于那些预算敏感、不能用 GPU 实例的 LLM 在线推理场景,Graviton4 是目前性价比最高的选择。
视觉推理我们用 ResNet50 v2,基于 ONNX Runtime 1.18.1 的 CPU EP,输入 224×224 张量,batch size=32。对比实例同 LLM 测试。结果如下:(延伸阅读:Graviton4迁移实测:推理成本降至x86的60%,但内存带宽瓶颈让我凌晨三点爬起来加监控)
| 实例 | 吞吐 (img/s) | 单图成本 (美分) |
|---|---|---|
| m7i.4xlarge | 1240 | 0.000173 |
| m7g.4xlarge | 1560 | 0.000116 |
| m8g.4xlarge(SVE2 ONNX) | 2010 | 0.000085 |
ResNet50 这类传统 CV 模型在 ARM 上表现更好,因为 ONNX Runtime 的 arm64 后端对卷积运算有深度优化,且 SVE2 可直接用于池化、激活等算子。我们线上目前跑着 20 个 ResNet 实例,全部迁移到 m8g 后,每月推理成本从 $4100 降到 $2400。
微服务那一侧:Spring Boot 和 Nginx 的压力测试
AI 推理只是我们工作负载的一半。另一半是近四十个基于 Spring Boot 3.2 和 Spring Cloud 的无状态微服务,以及前置的 Nginx 反向代理。我用了 wrk2 恒定连接速率压测,目标获取 JWT 验证后的 JSON 响应。在相同 16 vCPU 实例上,Java 应用采用 OpenJDK 21,开启 G1 GC,并设置 -XX:ActiveProcessorCount=16。Graviton4 在 p99 延迟上比 Intel 低 18%,吞吐高 22%。这跟 ARM 架构更适合高并发线程模型(尤其是虚拟线程)有关,Java 21 的虚拟线程在 ARM 上上下文切换开销更低。
但是,Nginx 的静态文件压测却出了问题。起初我们直接用默认的 nginx:1.26.0-alpine 镜像,Graviton4 的 QPS 只比 Intel 高 5%,与 CPU 规格提升完全不匹配。抓包发现 SSL/TLS 握手耗时占比过高。进一步排查发现,OpenSSL 3.0 的 ARM 构建默认用的是通用代码,没有启用 AES 和 SHA 硬件扩展。我用编译参数 -DCMAKE_C_FLAGS="-march=armv8.6-a+crypto+sve2" 自编译了一个 Nginx + OpenSSL 镜像,并开启 ssl_early_data on;,QPS 才从 3.2 万跳到 5.1 万,比 Intel 高 35%。
这个教训很典型:ARM 上的基础镜像如果不针对微架构编译,性能只能发挥六七成。而且必须做压测验证,光靠 CPU 利用率监控看不出这类“慢性病”。(延伸阅读:救命,Rust 1.85的异步闭包让我把1200行砍到200行,编译器再也不骂人了)
编译优化和 AI 加速库:那些让我半夜改代码的 ARM 专属调优
在 Graviton4 上跑 AI 推理,你绝对不能直接用 pip install 下来的通用 aarch64 wheel。PyTorch 官方提供的 ARM64 包虽然能跑,但默认的 ATen GEMM 是走无优化的 C++ 回退,除非你显式启用 MKLDNN(oneDNN)或 XNNPACK 后端。而我们发现,在 Graviton4 上,XNNPACK 对 SVE2 的利用比 oneDNN 更彻底,尤其是矩阵乘法和卷积。
我在推理服务里设置:
import torch
torch.backends.xnnpack.enabled = True
torch.backends.xnnpack.sve2_enabled = True
# 必须设置线程数,否则 XNNPACK 会过度订阅
torch.set_num_threads(8)
这样一来,PyTorch 在调用 torch.matmul 和 conv2d 时会尝试使用 XNNPACK 的 SVE2 内核,而不是 fallback。实测 Llama3-8B 的 PyTorch 版本推理 token 级延迟下降了 22%。但注意,如果你用的是 Transformers 库,模型内部某些算子可能不经过 XNNPACK,所以必须加一行全局替换:torch._C._jit_override_can_fuse_on_cpu(True),否则它会禁用部分融合。
对于 ONNX Runtime 推理,我们统一使用 CPU Execution Provider,并显式打开 SVE2 ACL 支持。构建时必须从源码编译 ONNX Runtime with ACL:
./build.sh --config Release --update --build --parallel --arm64
--build_wheel --enable_arm_compute_library --enable_sve2
--cmake_extra_defines ACL_ARCH=armv8.6-a+sve2
然后在代码里:
import onnxruntime as ort
sess_options = ort.SessionOptions()
sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_EXTENDED
sess_options.execution_mode = ort.ExecutionMode.ORT_SEQUENTIAL
# 强制使用 ACL,且线程数匹配核心
provider_options = {'AclUseSVE2': '1', 'AclThreadCount': '8'}
session = ort.InferenceSession('resnet50-v2-7.onnx', sess_options,
providers=['ACLExecutionProvider'],
provider_options=[provider_options])
经过这两项配置,ResNet50 推理吞吐从之前未优化时的 1580 img/s 提升到 2010 img/s,效果显著。
Go 和 Python 的编译标志陷阱
我们的微服务有一半是用 Go 1.22 写的。Go 编译器本身能自动为 arm64 启用 SIMD,但默认生成的二进制针对的是相对较老的 ARMv8.0。要让编译器产生 SVE2 指令,必须设置 GOARM64=v8.6 和 GOGCCFLAGS="-march=armv8.6-a+sve2"。我们在 Dockerfile 里加:
ARG TARGETARCH
ENV GOARCH=arm64
ENV GOARM64=v8.6
ENV CGO_ENABLED=1
ENV CGO_CFLAGS="-march=armv8.6-a+sve2 -O3"
构建后的二进制在 Graviton4 上跑,我们的业务逻辑(含大量 AES 加密和 JSON 序列化)吞吐提升了 17%,延迟波动也小得多。之前没用这个标志时,偶尔会因为 CPU 加密扩展未利用导致 p99 延迟突刺,把 Alertmanager 的告警拉高。
微服务网络的并发陷阱:NUMA 亲和性、内核参数和负载均衡的血泪经验
Graviton4 的设计是单 NUMA 节点(每个插槽一个 NUMA domain),但如果你买的是更大规格的实例如 m8g.16xlarge(64 vCPU),它实际上有两个物理插槽,两个 NUMA 节点。我们在压测 Nginx 时,把 16 个 worker 进程均匀分散在两个 NUMA 节点上,结果跨节点内存访问导致 p99 延迟急剧恶化。开始我没留意,因为 CPU 利用率才 60%,觉得不是瓶颈。直到用 perf stat -e node-loads,node-stores 看到大量远程内存访问,才明白是 NUMA 布局导致本地内存带宽饱和。
解决办法简单粗暴:用 numactl --cpunodebind=0 --membind=0 nginx 把 Nginx 绑定到单个 NUMA 节点,然后用流控把流量按实例分组,每个实例只服务同 NUMA 的请求。这样 p99.9 延迟从 420ms 降到 210ms。代价是硬件利用率只能到 50%,但稳定性优先,我们认了。
内核参数调优和 eBPF 监控的介入
迁移过程还踩了 TCP 内存的坑。默认的 net.core.rmem_max 和 wmem_max 太小,导致 Nginx 在瞬时高并发下出现 Recv-Q 堆积。我把这两个值调到 16777216,并打开 TCP Fast Open,QPS 又上去 8%。但这里我忘了加 Prometheus 针对 TCP 队列的告警,导致一次线上事故:凌晨三点,一个支付回调接口因为上游延迟导致 keepalive 连接数猛增,Recv-Q 堆积到 128,新的请求被丢弃,CPU 却正常。那晚的教训直接让我给所有实例部署了 eBPF 探针监控 TCP 重传和 backlog 深度,否则根本发现不了。
另一个必须调的是 Java 虚拟线程调度器与内核的配合。我们 Spring Boot 应用迁移到 Graviton4 后,发现虚拟线程在 NUMA 节点间迁移频繁,用 tuna 把 JVM 进程固定到核上,再加 -XX:ActiveProcessorCount=8(绑定 8 个核),吞吐反而比不绑核稳定 30%。因为虚拟线程的 carrier 线程如果跨 NUMA 调度,会拖慢整个调度周期。现在我统一在 systemd unit 里配 CPUAffinity=0-7 或 numactl --physcpubind=0-7 启动 Java 进程,并在 Prometheus 上监控 jvm_threads_pinned 指标。
成本效益分析:一张表看懂 Graviton4 的钱花在哪
综合上述所有测试,我把三类工作负载的日均成本(按 24 小时运行)整理出来,直观对比:
| 工作负载 | 实例规格 | 日成本 ($) | 性价比指数 (业务吞吐/$) |
|---|---|---|---|
| Llama3-8B 推理 (100万 requests/day) | 6× m7i.4xlarge | 110.59 | 1.0 (基线) |
| 4× m8g.4xlarge (优化后) | 58.94 | 2.87 | |
| ResNet50 推理 (2000万张图片/day) | 20× m7i.4xlarge | 368.64 | 1.0 |
| 12× m8g.4xlarge (优化后) | 177.15 | 3.10 | |
| Spring Boot 微服务集群 (4亿请求/day) | 40× m7i.xlarge | 737.28 | 1.0 |
| 30× m8g.xlarge (优化后) | 460.80 | 2.01 |
注意,Spring Boot 那里我用了 m8g.xlarge(4 vCPU)而非大规格,因为我们的微服务更适合横向扩展。在同样数量的 Pod 下,Graviton4 单 Pod 能承载更多 QPS,且抖动更小。整体算下来,如果我们把全部 AI 推理与微服务从 Intel 搬到 Graviton4 并做好编译与内核优化,每月可节省超过 $19000,同时性能还有提升。
我给出的迁移路线图很简单:先拿无状态、无 GPU 依赖的推理服务(如 ResNet)和 Spring Boot 服务开刀,直接换实例类型,然后重编译二进制并开启 SVE2 标志,压测验证。LLM 推理相对复杂,需要配合 llama.cpp 或 ONNX 优化,但一旦完成,收益最高。过程中必须配套 Prometheus 监控 CPU 指令集利用率(perf stat 集成)、NUMA 远程访问、JVM 虚拟线程状态,否则你根本不知道哪一步配置没生效。
Graviton4 不是银弹,但如果你愿意花时间把编译参数和内核设置调到匹配它,它能让你的云账单断崖式下降。而我,在经历那两个月的连续深夜报警后,已经把所有 Grafana 面板配上 ARM 专用指标,现在可以安心睡觉了。