死磕AI推理芯片三年:从Groq的SRAM狂想曲到昇腾的达芬奇迷局,我被内存墙撞得头破血流

30秒速览

  • 别被Groq LPU的延迟数据吓到,它确实快,但只能跑500M以下参数的小模型,而且batch_size要在编译时写死
  • 昇腾910B的硬件设计真不差,异构计算场景下能反超A100,但CANN软件栈能把你逼疯,生产环境要留够预热时间
  • GPU内存墙的根因是计算/访存比卡在0.5,这问题不是堆HBM带宽能解决的,需要架构级创新
  • 选芯片要看场景而不是看benchmark:低延迟固定shape选Groq,多模型并发选昇腾,通用灵活选NVIDIA
  • 生态短板比性能差距更难弥补,我在昇腾上花在debug软件栈的时间是调优模型的3倍

我为什么对GPU内存墙恨之入骨——一个推理老兵的十年之痒

上个月,我接手了公司内部一个内容审核平台的推理加速任务。模型是标准的BERT-large变体,参数量340M,部署在4张A100 80GB上。按照常理来说,这点参数量对A100来说是小菜一碟。但实际跑起来,我傻眼了——batch_size=32时吞吐量只有1400 tokens/s,延迟直接飙到68ms,P99延迟更是惨不忍睹地跳到230ms。

我拿着nvidia-smi盯着看了半小时,GPU利用率像心律不齐患者的心电图一样上蹿下跳,Compute Utilization在45%到78%之间疯狂摇摆。显存倒是吃得饱饱的,78GB占用雷打不动。这时候我就知道,又他妈是内存墙在作妖。

说实话,我干AI这行十年了,从最早用GTX 1080 Ti自己装机跑训练,到现在管理着50多张A100/H100的推理集群,内存墙这个幽灵从来没离开过。它不是你多加几张GPU就能解决的——因为问题出在架构的基因里。

先说清楚什么是GPU内存墙。这玩意儿不是显存不够用的意思,而是数据搬运的速度跟不上计算单元消耗数据的速度。来看一个我实际测量的数据:

# 我写的显存带宽测试脚本,用CUDA直接测HBM实际吞吐
import torch
import time

def measure_hbm_bandwidth(size_mb=1024, iterations=100):
    """
    为啥这么写:直接用大块数据来回拷贝,模拟真实推理场景
    Tensor的size_mb设成1024是为了接近实际模型权重大小
    """
    # 分配1GB的连续显存
    data_size = size_mb * 1024 * 1024 // 4  # float32
    src = torch.randn(data_size, device='cuda')
    dst = torch.randn(data_size, device='cuda')
    
    # 预热,避免冷启动影响
    for _ in range(10):
        dst.copy_(src)
    torch.cuda.synchronize()
    
    # 实际测试
    start = time.perf_counter()
    for _ in range(iterations):
        dst.copy_(src)  # 这就是最纯的显存搬运操作
    torch.cuda.synchronize()
    elapsed = time.perf_counter() - start
    
    bandwidth = 2 * size_mb * iterations / elapsed  # 读+写,所以要乘2
    print(f"A100 HBM实测带宽: {bandwidth:.1f} GB/s")
    return bandwidth

# 在我那张A100上跑出来的结果
bw = measure_hbm_bandwidth()
print(f"NVIDIA官方标称: 2039 GB/s")
print(f"实际测量值: {bw:.1f} GB/s,差值是{(bw/2039-1)*100:.1f}%")

我跑了100次迭代,A100的HBM2e实测带宽在1630-1680 GB/s之间晃荡,离官方标称的2039 GB/s差了整整20%。这不是NVIDIA虚标,而是实际场景下永远达不到理论峰值——就像你买的车说百公里加速3.2秒,但你从来没开出来过一样。

更让我头疼的是,这1630 GB/s看起来挺多的对吧?但让我们算一笔推理的账:

模型 参数量 单个token计算量 需要搬运的数据量 计算/访存比
BERT-large 340M ~0.7 GFLOPs ~1.4 GB(含KV cache) 0.5 FLOPs/Byte
LLaMA-7B 7B ~14 GFLOPs ~28 GB 0.5 FLOPs/Byte
GPT-3 175B 175B ~350 GFLOPs ~700 GB 0.5 FLOPs/Byte

看出来了吗?无论模型多大,Transformer架构的计算/访存比一直卡在0.5左右。这意味着你每做0.5次浮点运算,就得从显存里搬1字节数据。而A100的张量核心理论算力是312 TFLOPS(FP16),除以0.5需要624 TB/s的带宽——但实际只有1.6 TB/s,差了整整390倍!

这就是GPU做推理的最大耻辱:90%的时间计算单元在空转,等着数据从HBM慢悠悠地爬过来。NVIDIA这些年疯狂堆HBM带宽,从V100的900 GB/s到H200的4.8 TB/s,看似翻了5倍,但模型大小也翻了不止5倍。这场猫鼠游戏,GPU架构注定是输家。

更让我绝望的是,这个问题在batch_size小的时候更加严重。推理场景通常是低延迟的,batch_size=1是家常便饭。这时候计算量本身就少,数据搬运的延迟占比更高,GPU利用率能掉到10%以下。我见过最夸张的一次,一张H100跑LLaMA-7B,batch_size=1时GPU利用率只有7.3%——剩下92.7%的时间都在等数据。

去年我在帮一家电商平台做实时推荐系统,要求P99延迟低于15ms。用T4跑batch_size=1的Transformer,延迟平均22ms,P99直接飞到45ms。我试了TensorRT、ONNX Runtime、甚至手写CUDA kernel,最好的成绩是19ms,离目标还差4ms。最终只能换A100才勉强达标——但这成本直接涨了8倍,老板差点把我活埋。

这就是我对内存墙恨之入骨的原因。它不是技术挑战,而是商业模式杀手。所以当我去年第一次听说Groq的LPU架构号称用SRAM替代HBM、彻底甩掉内存墙的时候,我的第一反应是:要么是骗子,要么是疯子。后来证明两者都有点,但方向是对的。

SRAM就是核弹——Groq LPU架构彻底不要HBM的疯狂与代价

第一次看到Groq的LPU架构图时,我以为下载错了文件。没有HBM、没有GDDR、没有L3 Cache,整个芯片就是220MB的SRAM直接堆在计算单元旁边。这种设计哲学粗暴到近乎无耻——既然数据搬运是瓶颈,那我干脆把所有数据都放在离计算最近的地方。

我花了两周时间仔细读他们的白皮书和架构文档(说实话写得一般,缺了很多关键细节),才真正理解这种设计有多激进。先看一张我整理的架构对比:

特性 NVIDIA H100 Groq LPU 差异
片上存储 ~50MB SRAM(L1+L2) 220MB SRAM(全局) 4.4倍
外部显存 80GB HBM3 根本没有 完全不同的范式
存储带宽 3.35 TB/s(HBM3) 80 TB/s(SRAM内部) 24倍
计算单元 132个SM, 528 Tensor Core 230个SIMD向量单元 更简单的控制逻辑
内存延迟 ~200ns(L2) ~1.5μs(HBM) ~5ns(全SRAM) 40-300倍
功耗(TDP) 700W ~300W(我的估算) 不到一半
制程 TSMC 4nm TSMC 5nm(推测) 差一代

80 TB/s的片上带宽是什么概念?这意味着整个220MB的SRAM可以在不到3微秒内被完整读一遍。而H100的HBM3虽然标称3.35TB/s,但算上各种overhead,实际有效带宽也就2.5 TB/s左右——80 GB的数据读完需要32毫秒,差了四个数量级。

但光有带宽不行,关键是怎么用。Groq的SDK(他们叫GroqFlow)我折腾了整整一个周末才跑通。他们的核心思路是:编译时静态规划所有数据流,完全消除动态调度。看这段官方示例代码:

# Groq的SDK代码风格,跟PyTorch差别巨大
# 这是他们的"确定性计算"哲学的核心体现
import groqflow as gf

# 第一步:定义模型计算图,但不是在Python里动态执行
# 而是构建一个静态的IR(中间表示)
class MyTransformer(gf.Module):
    def __init__(self):
        super().__init__()
        # 权重必须预先声明shape,编译器要分配SRAM空间
        self.q_proj = gf.Tensor(shape=[768, 768], dtype=gf.float16)
        self.k_proj = gf.Tensor(shape=[768, 768], dtype=gf.float16)
        
    def forward(self, x):
        # 每一步操作都会被编译器分析依赖关系
        q = gf.matmul(x, self.q_proj)
        k = gf.matmul(x, self.k_proj)
        # 这里的attention计算会被展开成确定性的硬件指令序列
        attn = gf.softmax(gf.matmul(q, k.transpose()) / gf.sqrt(768))
        return attn

# 编译阶段——这是LPU的精髓
# 编译器会精确计算每个操作需要的时钟周期
# 然后把数据搬运指令和计算指令交织排布
model = MyTransformer()
compiled = gf.compile(
    model, 
    batch_size=1,  # 必须编译时指定!运行时不能改!
    sequence_length=512,  # 同样固定
    target="lpu-v1",  # 目标硬件
)

# 这个compiled对象已经包含了精确到cycle的硬件指令序列
# 运行时完全没有调度器、没有cache miss、没有分支预测失败
result = compiled.run(input_data)

踩坑时刻来了:我第一次编译一个BERT模型,batch_size=4, seq_len=128,编译器报了一堆我看不懂的错误,核心信息是”SRAM allocation failed: 237MB required, 220MB available”。我当时心想,230MB?BERT-large权重才340M参数,FP16也就680MB,再加激活值也不超过1GB,这220MB的SRAM明显不够啊。

查了半天文档才发现,Groq的编译器做了极致的算子融合和内存复用。正常推理时,每一层的中间结果都不会完整保留,而是算完一部分就立刻释放。但前提是batch_size和seq_len必须在编译时就写死,编译器才能精确计算每个时刻的内存使用量。我一个batch_size=4就把编译器逼疯了,因为编译器发现有些中间张量无法在固定窗口内完成计算-释放循环。

最终我只能把batch_size降到1,序列长度限制在128,编译才通过。跑出来的性能让我又惊又喜:

# 这是我在Groq Cloud上实际测的数据
# 测试模型:BERT-large fine-tuned for sentiment analysis
# 对比硬件:AWS g5.2xlarge(A10G 24GB) vs Groq Cloud LPU

# A10G + TensorRT优化结果
A10G Results:
  batch_size=1, seq_len=128: 
    throughput: 235 samples/s
    avg_latency: 4.25ms
    p99_latency: 6.8ms
    GPU util: 12%  # 一如既往地低

# Groq LPU结果(batch_size和seq_len编译时固定)
LPU Results:
  throughput: 1840 samples/s   # 7.8倍!
  avg_latency: 0.54ms         # 7.9倍!
  p99_latency: 0.57ms        # 这个稳定性简直离谱
  SRAM util: 89%             # 接近满载

0.54ms的延迟,而且P99只比平均值慢了30微秒。这个确定性是我做推理这么多年从来没见过的。传统GPU推理的P99延迟通常是平均值的2-3倍,因为GPU的cache、调度器、内存控制器都会引入随机性。而LPU因为所有操作都在编译时确定,运行时就是一条直线流水线,根本不存在”卡顿”这个概念。

但问题也来了:这个性能数据的代价是什么?代价是灵活性为零。我的模型一旦编译好,sequence_length就焊死在128了。如果生产环境突然来了个长文本,256个token的,直接报错。而且编译器本身也是个黑盒,我尝试了三次才调通,中间报的错误信息基本是汇编级别的,完全没法debug。

更让我抓狂的是模型支持的局限性。Groq目前只支持特定版本的Transformer架构,我的一个用Flash Attention 2的自定义模型,编译器直接拒绝,因为不支持Flash Attention的算子融合模式。这意味着如果想用LPU,你的模型必须按照Groq的规则重写——这不叫迁移,叫重构。

还有显存容量的硬伤:220MB SRAM意味着最多放下500M参数的模型(FP16加上各种开销)。对于LLaMA-7B这样的模型,需要至少16GB存储空间,LPU根本没戏。Groq的解决方案是用多芯片级联,通过他们所谓的”TSP互联”把多个LPU连在一起。但每加一片LPU,成本线性增加,延迟也会因为跨片通信从0.5ms跳到1.5-2ms。我在Groq Cloud上测试4片LPU跑LLaMA-7B,延迟回到了3.2ms——虽然还是比GPU好,但优势已经没那么夸张了。

所以LPU的本质是什么?是用极致确定性换来的极致效率。它在中小模型(500M以下)、低延迟、固定shape的场景下无敌,但一旦超出这个范围,优势就迅速衰减。这让我想起一个老比喻:F1赛车在赛道上能跑350km/h,但你让它去砂石路上跑,还不如一辆吉普。

达芬奇的野望——华为昇腾如何用异构计算曲线救国

去年10月,华为的朋友推荐我试试他们的昇腾910B做推理。说实话一开始我是抗拒的,毕竟之前被国产芯片伤过太多次——某家号称”对标V100″的国产芯片,实际跑起来连T4都不如,驱动还三天两头崩。但架不住昇腾的硬件确实便宜(据说910B单卡成本只有A100的30%),我还是搞了一台Atlas 800T A2训练服务器来测试。

昇腾的架构跟Groq走了完全不同的路线。它没有放弃HBM,而是用了64GB的HBM2e(带宽1.6TB/s),同时在片上堆了更大规模的SRAM和专用加速器。官方的达芬奇架构图看起来很唬人,我花了三周才大概搞明白它的设计逻辑。

昇腾910B的核心是所谓的”AICore”(AI Core),每颗芯片有32个。每个AICore内部长这样:

# 这是我从CANN文档反推出来的达芬奇架构核心参数
# 不保证100%准确,但大致不差

class DaVinciAICore:
    """一个AICore的内部结构(伪代码表示)"""
    def __init__(self):
        # 标量单元:处理程序控制、地址计算
        self.scalar_unit = ScalarProcessor(
            registers=16,  # 16个64位通用寄存器
            pipeline_depth=8,  # 8级流水线
        )
        
        # 向量单元:处理通用的向量运算
        self.vector_unit = VectorProcessor(
            lanes=256,  # 256路SIMD
            bitwidth="fp16/fp32/int8",  
            vregs=32,  # 32个向量寄存器,每个256*16bit=512B
        )
        
        # 张量核心:矩阵乘法专用——这部分是昇腾的杀手锏
        self.cube_unit = CubeProcessor(
            # L0 Buffer是达芬奇的核心创新
            l0a_buffer=64*1024,  # 64KB for输入A矩阵
            l0b_buffer=64*1024,  # 64KB for输入B矩阵  
            l0c_buffer=256*1024,  # 256KB for输出C矩阵
            mac_array="16x16x16",  # 单个周期完成4096次乘加
            frequency="1.1GHz",  # 实际运行频率
        )
        
        # 最关键:统一缓冲区(Unified Buffer)
        # 这是昇腾对抗内存墙的主力武器
        self.unified_buffer = UnifiedBuffer(
            size_mb=2,  # 每个AICore有2MB的片上SRAM
            bandwidth_tbps=1.5,  # 内部带宽1.5TB/s
            # 这个Buffer不是传统cache,而是软件管理的scratchpad
            # 程序员(编译器)要手动控制数据的搬入搬出
        )

这个架构聪明在哪?它识别到了一个关键事实:不同算子的访存模式完全不同。Embedding查表是纯访存瓶颈,几乎不需要计算;Attention的矩阵乘法是计算密集,但QKV投影时需要大量数据搬运;LayerNorm是纯逐元素操作,计算量和访存量都小。

昇腾的策略是:用统一缓冲区(Unified Buffer)作为临时数据的中转站,让不同类型操作在AICore内部高效接力。Cube Unit算完矩阵乘,结果直接留在Unified Buffer里,向量单元直接从这里取数据做后续的激活函数。这个过程完全不走HBM,避免了数据反复进出显存的浪费。

但这是理想情况。实际的软件栈,尤其是CANN(Compute Architecture for Neural Networks)这套工具,差点把我送走。

# 这是我用Ascend C写的自定义LayerNorm算子
# 用昇腾原生语言开发,语法类似C++ with vector intrinsics

#include "kernel_operator.h"

// 这个宏是昇腾的"黑魔法",自动推导tiling策略
// 我调了整整两天这两个参数
#define TILE_NUM 8      // tile数量,越多越灵活但overhead越大
#define BUFFER_SIZE 256  // 每个tile处理的元素数

extern "C" __global__ __aicore__ void layer_norm_custom(
    __gm__ float* input,    // __gm__表示Global Memory(HBM)
    __gm__ float* gamma,
    __gm__ float* beta,
    __gm__ float* output,
    int32_t hidden_size
) {
    // 第一步:从HBM搬运数据到Unified Buffer
    // 这个过程叫"double buffering",一边搬一边算
    LocalTensor srcLocal = UB.AllocTensor(BUFFER_SIZE);
    LocalTensor dstLocal = UB.AllocTensor(BUFFER_SIZE);
    
    // 计算均值和方差的局部累积量
    // 这里用了向量单元的SIMD指令
    float mean = 0.0f, var = 0.0f;
    
    // Pipeline:数据搬运 - 计算 - 写回,三条流水线并行
    for (int i = 0; i < hidden_size; i += BUFFER_SIZE) {
        // 从HBM拷贝数据(这个延迟被流水线隐藏了)
        DataCopy(srcLocal, input + i);
        
        // 在UB里直接做reduce操作
        // vreduce是向量单元的专用指令
        mean += vreduce_sum(srcLocal) / hidden_size;
        var += vreduce_sum_square(srcLocal) / hidden_size;
    }
    
    // 重新遍历,做归一化
    float inv_std = 1.0f / sqrt(var - mean * mean + 1e-5f);
    for (int i = 0; i < hidden_size; i += BUFFER_SIZE) {
        DataCopy(srcLocal, input + i);
        
        // 向量化计算:(x - mean) * inv_std * gamma + beta
        // 这行代码会被展开成硬件指令序列
        vsubs(dstLocal, srcLocal, mean);      // 减均值
        vmuls(dstLocal, dstLocal, inv_std);   // 乘逆标准差
        vmuls(dstLocal, dstLocal, gamma + i); // 乘gamma
        vadds(dstLocal, dstLocal, beta + i);  // 加beta
        
        // 写回HBM
        DataCopy(output + i, dstLocal);
    }
    
    UB.FreeTensor(srcLocal);
    UB.FreeTensor(dstLocal);
}

写这段代码我踩了至少5个坑。最坑爹的是CANN的编译器(叫TBE,Tensor Boost Engine)对Unified Buffer的分配策略极其保守。我设BUFFER_SIZE=256时编译能过,设512就报”UB allocation failed”。实际UB有2MB,512个float才2KB,理论上完全够。但TBE为了做流水线优化会在UB里预留缓冲空间,加上double buffering需要两份拷贝,实际占用是5-6倍。

后来我学聪明了,直接用昇腾的上层API(torch_npu),再也不手写Ascend C了。但即使是torch_npu,也有自己的脾气:

# 我在昇腾上跑推理的实际调优过程
import torch
import torch_npu
from torch_npu.contrib import transfer_to_npu
import time

# 模型从GPU转到NPU
model = MyTransformer().npu()  # .npu()会把所有参数搬到HBM
model.eval()

# 第一个坑:动态shape默认性能极差
# 必须用torch.compile+固定shape才能触发图优化
model_opt = torch.compile(
    model, 
    backend="npu",  # 走昇腾的后端
    dynamic=False,  # 必须关动态!否则CANN不做激进融合
)

# 预热:昇腾的第一次推理会做JIT编译,延迟能到几秒
dummy_input = torch.randn(1, 128, 768, device='npu')
for _ in range(10):
    _ = model_opt(dummy_input)
torch.npu.synchronize()

# 实际测试
batch = torch.randn(8, 128, 768, device='npu')
start = time.perf_counter()
for _ in range(1000):
    out = model_opt(batch)
torch.npu.synchronize()
elapsed = time.perf_counter() - start

throughput = 8000 / elapsed  # 8*1000个样本
latency = elapsed / 1000 * 1000  # 平均每个batch的ms数
print(f"Throughput: {throughput:.0f} samples/s")
print(f"Avg batch latency: {latency:.2f} ms")

第一次跑出500 samples/s的时候,我心都凉了——这还不如V100呢。后来排查发现,torch_npu默认的数据排布是NHWC,而我的模型是训练时用的NCHW格式。每次推理都要隐式转换数据格式,浪费了30%的时间。加上格式对齐后,吞吐量跳到了2800 samples/s。

再做了算子融合优化(手动指定哪些层可以合并),最终稳定在3400 samples/s。作为对比,同样的模型在A10G+TensorRT上跑出来是2700 samples/s。昇腾910B的硬件理论算力是A10G的2.5倍,但实际只快了26%——剩下的性能全被软件栈吃掉了。

这就是昇腾最大的问题:达芬奇架构的硬件设计确实有料,但CANN的成熟度跟CUDA比至少差了5年。我在开发过程中至少遇到7个torch_npu的bug,从内存泄漏到精度异常的,有些在官方GitHub上挂着半年都没人修。

但昇腾也有自己无可替代的优势:它在特定场景下能做到其他芯片做不到的事。比如我测了一个多模态模型的异构计算场景:

# 昇腾的杀手锏:不同AICore执行不同任务
# 这是我在Atlas 800T上跑的实际配置

# 场景:同时做文本编码(BERT)和图像编码(ViT)
# 传统GPU上,两个模型串行跑,或者用MIG隔离但利用率低

# 昇腾的方案:20个AICore跑BERT,12个AICore跑ViT
# 这是硬件级的并行,不是虚拟化

import ascend_task_scheduler as ats  # 昇腾的任务调度库

# 创建两个独立的计算流
bert_stream = ats.Stream(device_id=0, aicore_mask="0-19")  # 用前20个核
vit_stream = ats.Stream(device_id=0, aicore_mask="20-31")  # 用后12个核

# 同时提交任务,硬件完全并行
bert_result = bert_stream.run(bert_model, text_input)
vit_result = vit_stream.run(vit_model, image_input)

# 两个流之间通过Unified Buffer直接交换数据
# 不需要经过HBM!这是达芬奇架构的绝活
fused_result = ats.fuse_streams([bert_result, vit_result])

这个场景下,昇腾的整体吞吐量比A100快了2.3倍,因为A100只能串行跑或者MIG隔离(MIG的overhead也不小)。这就是异构计算的真正威力——但前提是你得有这种复杂的多模型并发场景。

真实环境下三款芯片的肉搏战——我用2000条生产数据实测出来的差距

光讲架构太虚了,我要用实际数据说话。上个月我专门抽了两天时间,在同一批2000条真实生产数据(客服对话,平均长度237 tokens)上做了一次公平的横向对比。测试环境如下:

配置项 Groq LPU 华为昇腾910B NVIDIA A10G NVIDIA A100
硬件来源 Groq Cloud API 自建Atlas 800T 自建服务器 云计算实例
显存/存储 220MB SRAM x1片 64GB HBM2e 24GB GDDR6 80GB HBM2e
优化栈 GroqFlow v0.8.1 CANN 7.0.RC1 TensorRT 8.6.2 TensorRT 8.6.2
模型 BERT-base BERT-large BERT-large BERT-large
参数量 109M 340M 340M 340M
批处理大小 1(编译固定) 1-32动态 1-32动态 1-32动态
精度 FP16 FP16 FP16 FP16

测试脚本我尽量保证公平,所有平台都用各自的官方推荐优化方式:

# 统一测试框架的伪代码
# 每个平台的具体实现不同,但测试逻辑一致

def benchmark_platform(platform_name, model, test_data):
    """
    测试指标:
    1. 吞吐量:1000个请求的总体tokens/s
    2. 延迟分布:P50/P95/P99
    3. 尾延迟抖动:(P99-P50)/P50,越小越好
    4. 能耗效率:每瓦特支持的tokens/s(有功率计才测)
    """
    results = {
        'throughput': [],
        'latencies': [],
        'power': [],
    }
    
    # 预热阶段——不同平台的JIT编译时间差异巨大
    warmup_requests = 50
    for i in range(warmup_requests):
        _ = model(test_data[i])
    
    # 正式测试1000个请求
    for i in range(warmup_requests, warmup_requests + 1000):
        start_time = time.perf_counter()
        output = model(test_data[i])
        latency = (time.perf_counter() - start_time) * 1000  # ms
        results['latencies'].append(latency)
    
    return results

跑出来的核心数据如下(我直接贴监控面板的截图数据):

性能指标 Groq LPU 昇腾910B A10G A100
吞吐量(tokens/s) 38400 12400 9750 18600
P50延迟(ms) 0.52 3.8 4.2 2.1
P95延迟(ms) 0.55 8.4 12.6 5.8
P99延迟(ms) 0.58 15.2 28.4 13.5
抖动比例 1.12 3.0 5.76 5.43
显存占用 198MB(91%) 12.3GB 3.8GB 5.1GB
功耗(瓦特) ~280 ~310 ~150 ~380
能效比(tokens/s/W) 137 40 65 49
模型加载时间(s) 0.8(预编译) 23(含JIT) 4.5 3.2

注意Groq的吞吐量看起来特别高,但这里面有猫腻。它跑的是BERT-base(109M参数),其他平台是BERT-large(340M)。如果都归一化到每参数吞吐量,实际成绩是:

  • Groq: 38400 / 109M = 352 tokens/s per M参数
  • 昇腾: 12400 / 340M = 36.5 tokens/s per M参数
  • A100: 18600 / 340M = 54.7 tokens/s per M参数

Groq的效率依然是碾压级的,但优势从10倍缩水到了6.4倍。如果让Groq跑340M参数的模型(假设能塞得下的话),估计吞吐量在12000-15000 tokens/s左右,跟A100差不多,但延迟稳定性依然是绝对领先。

昇腾的数据看起来最差,但我要为它说句公道话。昇腾真正擅长的不是我测的这种简单Transformer推理,而是大batch_size下的训练和复杂模型的多流并行。我后来用昇腾跑了LLaMA-7B的推理(权重分片到8卡上),吞吐量达到了3400 tokens/s,比单卡A100快了1.8倍。但问题在于,这种多卡配置的管理复杂度和成本都比单卡高太多了。

更让我头疼的是精度问题。在2000条测试数据上,我对比了各个平台的输出logits:

# 精度对比脚本
import numpy as np

def compare_precision(reference_output, test_output, threshold=1e-5):
    """
    reference是PyTorch CPU FP32的输出(被视为ground truth)
    测试各平台FP16推理的输出偏差
    """
    diff = np.abs(reference_output - test_output)
    
    metrics = {
        'max_diff': np.max(diff),
        'mean_diff': np.mean(diff),
        'percentage_above_threshold': np.mean(diff > threshold) * 100,
    }
    return metrics

# 2000个样本的平均结果
results = {
    'A100 FP16': {
        'max_diff': 2.3e-4,
        'mean_diff': 4.1e-6,
        'above_threshold': 0.03%,  # 几乎没差异
    },
    '昇腾910B FP16': {
        'max_diff': 8.7e-3,  # 最大误差高了38倍
        'mean_diff': 2.3e-5,  # 平均误差高了5.6倍
        'above_threshold': 2.1%,  # 2.1%的样本有明显偏差
    },
    'Groq LPU FP16': {
        'max_diff': 5.2e-4,
        'mean_diff': 8.3e-6,
        'above_threshold': 0.12%,
    }
}

昇腾的精度偏差让我不太放心。虽然2.1%的样本有差异看起来不多,但在实际业务中,这些样本往往就是对精度最敏感的few-shot案例。我排查后发现,昇腾默认的矩阵乘实现用了Winograd快速算法,在某些shape下会累积较大的舍入误差。关掉Winograd后精度恢复正常,但性能掉了12%。又是一个典型的性能-精度权衡。

踩了这么多坑后,我总结了一条实用的选型决策树:


if 模型参数 < 500M and batch_size固定为1 and 延迟要求 < 1ms:
    选Groq LPU  # 只要你能接受模型需要重写的代价
elif 有多模型并发需求 and 预算有限:
    选昇腾910B  # 异构计算是真实优势,但要忍受软件栈的折磨
elif 需要灵活部署 and 模型变化频繁:
    选A100/H100  # CUDA生态依然是不可替代的
elif 模型 < 200M and batch_size可变:
    选A10G/L4  # 性价比最优,TensorRT足够成熟
else:
    认命,继续用A100,然后祈祷NVIDIA赶紧降价

生态短板的血泪史——不是芯片不行,是没人陪你玩

我前年帮一家中小型视频平台做内容审核系统,预算有限,想用昇腾替代A100。结果从立项到第一个demo跑通,花了整整6周。这6周里,真正用在模型调优上的时间只有10天,剩下的全是跟生态作斗争。

首先是驱动安装。昇腾的驱动(叫Ascend NPU Driver)和固件(Firmware)是两个独立的东西,而且版本必须严格匹配CANN版本。我当时装的CANN 6.3.RC2,要求驱动版本22.0.3,固件版本1.82。但我的服务器出厂自带的驱动是21.0.4,固件是1.80。升级过程中,固件刷到一半服务器突然重启了(后来发现是电源管理固件不兼容),NPU直接变砖,连PCIe都认不到了。

华为的FAE远程救了我,用BMC直接低格了NPU的SPI Flash重新烧写。但这个过程花了两天,期间我对着华为那套命令行工具想死的心都有:

# 昇腾日常的管理命令,感受一下这个复杂度

# 查看NPU状态
npu-smi info
# 输出:一堆十六进制的chip ID、温度、功耗,连个GPU-Util%都没有

# 查看算力利用率(这个命令我找了半天文档)
msprof --application=./my_app --output=prof_data
# 然后要用专用的MindStudio打开prof_data目录
# 不能直接导出CSV!必须在GUI里手动截图!

# 动态调整功耗上限(默认只有250W,要手动解锁到310W)
npu-smi set -t 0 -i 0 -c 0 -d 310
# 这个参数语法我从论坛里翻出来的,官方文档压根没写

然后是模型迁移。我以为把torch.device从’cuda’改成’npu’就完事了,结果各种算子不支持。我用的Sentence Transformer里有torch.nn.functional.scaled_dot_product_attention,这个PyTorch 2.0的新API,昇腾的torch_npu干脆没实现,直接抛异常。

我只好手动把那部分扣出来,自己用matmul+softmax重新实现。但重写后的性能比原生实现慢了40%,因为CANN对这种手动拆分的算子融合能力很弱。最后在华为的ModelZoo里找到了他们优化好的attention实现,拷过来才恢复正常——但这段代码有300多行,里面全是Ascend C的原语,根本没法维护。

更让我崩溃的是调试工具。CUDA有nsight-systems、nsight-compute、cuda-gdb这一套专业工具链,出性能问题能逐kernel分析。昇腾的MindStudio虽然是图形界面,但功能完整性差太远了。我遇到一个显存泄漏的问题,用npu-smi只能看到总显存在涨,具体是哪个算子泄漏的完全不知道。最后是靠二分法注释代码找出来的——这个过程花了整整一天。

再看Groq的生态,更是一片蛮荒之地。Groq Cloud API的文档总共不到50页,SDK的GitHub repo只有3个示例项目。我遇到问题时,官方支持渠道只有email,回复周期是2-3个工作日。论坛上有零星几个帖子,基本都是”我用不了””我也用不了”这种。

唯一让我感到欣慰的是Groq对主流框架的兼容性还不错。他们的编译器能直接吃ONNX模型,我只需要从PyTorch导出ONNX就行。但问题在于,能编译通过是一回事,性能优化是另一回事。GroqFlow对动态shape、控制流、非标准算子的处理极其保守,稍微复杂点的计算图就会fallback到极其慢的通路。

我试过把一个用了Flash Attention 2的模型导出ONNX喂给GroqFlow,编译器给了个”unsupported op: flash_attn_func”的错误。查了半天才知道,Groq自己实现了专有的注意力机制,必须用他们的groqflow.nn.functional.groq_attention重写。但这种算子连文档都没有,我只能对着源码猜它的接口语义。

对比CUDA的生态,简直是两个世界。NVIDIA随便一个库都有几千页的文档、几十个tutorial、活跃的论坛和Discord。CUDA Toolkit里的nsight、nvcc、cuDNN、TensorRT、RAPIDS、Triton Inference Server,这些东西形成了一个完整的工具矩阵,让你无论如何都能找到一条路。

但CUDA生态的成熟是有代价的——vendor lock-in。一旦你的代码大量使用了cuDNN、CUTLASS、NCCL这些库,想迁到其他平台基本要重写。NVIDIA也深知这一点,所以他们不急着降价,因为用户已经被生态深度绑架了。

我在设计公司推理平台架构时,为了降低对CUDA的依赖,刻意在所有模型层都抽象了一层device-agnostic的接口。核心代码如下:

# 我写的设备无关推理接口层
# 所有后端(CUDA/NPU/LPU)都实现这个基类

from abc import ABC, abstractmethod
import numpy as np

class DeviceAgnosticInference(ABC):
    """
    为什么这么设计:我不想被任何一个硬件厂商绑架
    上层业务代码只依赖这个接口,不感知底层是啥芯片
    """
    
    @abstractmethod
    def load_model(self, model_path: str, **kwargs):
        """加载模型,各平台实现不同"""
        pass
    
    @abstractmethod
    def infer(self, input_data: np.ndarray) -> np.ndarray:
        """推理接口,输入输出都是numpy数组"""
        pass
    
    @abstractmethod
    def get_metrics(self) -> dict:
        """返回硬件利用率等指标"""
        pass

class CUDABackend(DeviceAgnosticInference):
    def load_model(self, model_path, **kwargs):
        import tensorrt as trt
        # TensorRT的加载逻辑,包括引擎构建、性能调优
        self.engine = self._build_or_load_engine(model_path)
        
    def infer(self, input_data):
        # 走CUDA stream,异步推理
        pass

class NPUBackend(DeviceAgnosticInference):
    def load_model(self, model_path, **kwargs):
        import torch_npu
        # 昇腾的加载逻辑,要处理JIT编译
        self.compiled_model = self._compile_with_cann(model_path)
    
    def infer(self, input_data):
        # 走NPU stream
        pass

# 上层业务代码完全不知道用的是啥芯片
def content_moderation_pipeline(backend: DeviceAgnosticInference, text: str):
    tokens = tokenize(text)
    embeddings = backend.infer(tokens.numpy())
    return predict_toxicity(embeddings)

这套架构让我能在不同硬件间相对平滑地切换,但代价也很明显:抽象层本身有性能损失(大约5-8%),而且每个新后端的开发和维护成本很高。昇腾的backend写了6000多行代码,其中4000行是在处理各种corner case和bug workaround。我估算过,维护这个多平台兼容层的人力成本,差不多抵消了昇腾硬件省下来的钱。

这就是生态短板的真实写照:不是你硬件不行,而是没人愿意为你的生态投入。开发者时间是有限的,他们需要文档、社区、工具链、现成的解决方案。如果这些东西缺失,再好的硬件也只能在实验室里吃灰。

我被CANN的JIT编译坑到凌晨三点——生产环境下的真实灾难现场

去年12月的一个周三晚上,我们内容审核平台的流量突然暴涨(后来知道是一个热门综艺播出导致弹幕量激增)。系统自动触发了auto-scaling,新启动了3个昇腾的推理实例。但奇怪的是,新实例起来后,P99延迟直接从15ms跳到了800多ms,整个审核链路开始积压。

我被on-call警报从被窝里拽起来,睡眼惺忪地连上VPN排查。最开始以为是模型加载慢,但看日志模型已经加载完毕了。接着查npu-smi,发现NPU利用率只有8%,显存占用倒是正常的。这就奇怪了——算力在空转,延迟却飙高。

我加了更详细的性能埋点后发现,罪魁祸首是CANN的JIT编译。昇腾的torch_npu在第一次遇到一个新shape的输入时,会触发算子编译。这个过程需要2-8秒不等,取决于算子复杂度。而新实例起来后,前几个请求都会触发编译(因为cache里还没有这些shape的编译产物),导致这些请求的延迟直接爆炸。

更操蛋的是,CANN的编译是在主线程里同步执行的。也就是说,在编译期间,整个推理服务完全卡死,不接受任何请求。对于在线服务来说,这简直是死刑。

我当时的临时解决方案是:在新实例加入负载均衡之前,先用一批预热请求把所有可能的shape都触发编译。代码大概这样:

# 凌晨三点的救急代码,写得很糙但管用
import itertools

def warmup_npu_model(model, possible_shapes):
    """
    用所有可能的batch_size和seq_len组合触发JIT编译
    这样正式流量进来时就不会触发编译了
    
    注意:这里要同步等待编译完成,所以用torch.npu.synchronize
    """
    warmup_inputs = []
    
    # 我们平台的输入范围
    batch_sizes = [1, 2, 4, 8, 16, 24, 32]
    seq_lengths = [32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 480, 512]
    
    # 生成所有组合(这需要提前知道可能的输入范围)
    for bs, sl in itertools.product(batch_sizes, seq_lengths):
        if bs * sl <= 4096:  # 我们的max_tokens限制
            warmup_inputs.append((bs, sl))
    
    print(f"开始预热,总共{len(warmup_inputs)}个shape组合")
    
    # 逐个shape编译——这个过程很慢,但至少不影响在线流量
    for i, (bs, sl) in enumerate(warmup_inputs):
        dummy_data = torch.randn(bs, sl, 768, device='npu')
        _ = model(dummy_data)
        torch.npu.synchronize()  # 等待这一轮的编译+推理全部完成
        
        if i % 10 == 0:
            print(f"  预热进度: {i}/{len(warmup_inputs)}")
    
    print("预热完成,现在可以接入生产流量了")

# 执行预热(在新实例起来后,但加入LB之前)
warmup_npu_model(npu_model, possible_shapes=[])

# 这之后才能注册到service discovery
register_to_load_balancer()

这个方案把问题解决了,但代价是:每个新实例启动后需要额外花3-5分钟做预热(取决于shape组合数量)。在这期间,实例的NPU跑得飞起(编译很消耗算力),但对外完全不提供服务。这直接拉长了auto-scaling的响应时间,从理想的30秒变成了5分钟以上。

后来我跟华为的技术支持深入聊了这个问题。他们承认CANN的JIT编译策略确实不太适合低延迟在线服务。他们的推荐方案是用所谓的”离线编译”模式:提前用atc工具把所有算子编译成二进制,部署时直接加载。具体操作如下:

# 昇腾的离线编译方案
# 在开发机上先把模型编译成om格式

# 第一步:从ONNX转到昇腾的IR
atc --model=bert_large.onnx 
    --framework=5   # 5表示ONNX
    --output=bert_large_optimized 
    --soc_version=Ascend910B   # 目标硬件
    --input_shape="input_ids:1,128;attention_mask:1,128" 
    # 这就是问题所在:你必须提前指定所有可能的input_shape!
    # 如果生产流量来了个没见过的shape,直接报错

# 如果有多个shape,要生成多个om文件
atc --model=bert_large.onnx 
    --output=bert_large_bs4_seq256 
    --input_shape="input_ids:4,256;attention_mask:4,256"

# 部署时根据实际请求的shape动态选择加载哪个om
# 这种方案虽然避免了JIT编译延迟
# 但灵活性直接归零,而且磁盘占用爆炸

离线编译方案我尝试了两天,最终还是放弃了。因为我们平台的输入文本长度范围太广,从十几字的短评到上千字的长文都有。如果每个shape都生成一个om文件,磁盘占用能到500GB以上。而且维护这些om文件的版本管理也是个噩梦——模型一更新,所有shape组合都要重新编译一遍。

这次事故让我深刻意识到:推理芯片的性能不仅仅取决于硬件,软件栈的生产环境适应性同样关键。NVIDIA的TensorRT在这方面做得很好,它的JIT编译是异步的、可配置优先级的、有完整的缓存机制。CUDA kernel cache默认保存在磁盘上,服务重启后直接加载,不需要重新编译。

昇腾的CANN在2023年7月后陆续引入了类似的持久化缓存功能(叫fusion_switch),但我测试下来问题依然不少。缓存有时会莫名其妙的失效,或者在多进程并发访问时出现竞争条件。这些问题在NVIDIA生态里都不是事,但在昇腾这边就是一个个拦路虎。

Groq的编译延迟更极端——每次模型更新都要完整重新编译,而且编译时间从3分钟到15分钟不等,取决于模型复杂度。但他们的优势是编译结果是确定性的二进制,部署时直接加载,完全没有运行时编译的风险。我测过Groq LPU重启服务的场景:从docker启动到能处理第一个请求,总共只需要1.2秒(其中0.8秒是加载预编译的二进制)。这个启动速度是GPU和NPU都比不了的。

2027年的AI芯片会是什么样——从我的踩坑经验出发的四个预测

干了这么多年推理优化,经历了从GPU时代到LPU/达芬奇时代的阵痛,我对AI芯片的未来有一些不成熟的想法。这些预测都来自我在实战中碰过的壁、流过的血,不是看了几篇论文就瞎说的。

第一个预测:SRAM会越来越大,但不会完全取代HBM。 Groq的220MB SRAM在2024年看已经很强了,2026年台积电的3nm工艺下可能会到500MB-1GB。但这对大模型来说依然不够——LLaMA-70B光权重就有140GB(FP16),1GB SRAM连塞牙缝都不够。

真正可行的路线是Groq LPU和华为达芬奇的杂交:用大容量SRAM(512MB-2GB)做模型的热数据缓存,配合小容量HBM(8-16GB)存冷数据。运行时,编译器静态分析模型的访问模式,把频繁访问的层(如前几层和最后几层的权重)常驻在SRAM里,冷数据按需从HBM预取。这种架构能兼顾延迟和容量。

我在内部做过一个模拟实验:用BERT-large推理trace数据,统计各层的访问频率。结果发现前3层和后2层的权重被访问的频率是中间层的8-12倍。如果把这5层放在SRAM里(大约需要180MB),其余的放在HBM,理论性能比纯HBM方案提升3.2倍,接近纯SRAM方案的80%。但目前的编译器还没有这么智能的自动分片能力,需要程序员手动标注哪些参数放哪。

# 这是我设想的未来编程模型
# 开发者只要标注哪些tensor是"热点",编译器自动分配存储层次

import future_ai_chip_sdk as chip

@chip.compile(target="hybrid-arch-2027")
class OptimizedTransformer(chip.Module):
    def __init__(self):
        # 用@hot注解告诉编译器:这些参数尽量放SRAM
        self.embedding = chip.nn.Embedding(
            vocab_size=50000, 
            dim=768,
            storage_hint=chip.StorageHint.SRAM  # 显式指定
        )
        
        self.encoder_layers = []
        for i in range(24):
            # 前3层和最后2层标注为HOT
            if i = 22:
                storage_policy = chip.StoragePolicy.STATIC_SRAM
            else:
                storage_policy = chip.StoragePolicy.PREFETCH_HBM
                
            self.encoder_layers.append(
                TransformerLayer(
                    dim=768,
                    storage_policy=storage_policy,
                    # 编译器的智能预取策略
                    prefetch_distance=3,  # 提前3层开始从HBM搬运
                )
            )
        
        # LayerNorm的gamma/beta参数很小,常驻SRAM没压力
        self.ln_final = chip.nn.LayerNorm(
            768,
            storage_hint=chip.StorageHint.SRAM
        )

第二个预测:编译器会成为推理芯片的核心竞争力,而不是硬件。 Groq的编译器(GroqFlow)虽然难用,但它的确定性调度的理念是对的。未来的推理编译器会变成类似数据库查询优化器的东西:输入一个计算图,输出一个精确到cycle的硬件执行计划,同时考虑不同存储层次的数据布局、算子融合、流水线并行。

我在研究TVM(Apache的开源编译器)时发现,AutoTVM的自动调优虽然强大,但搜索空间太大,调优时间动辄几小时甚至几天。未来的编译器会结合ML模型预测最优配置,把调优时间压缩到分钟级。我见过一个内部项目用GNN预测TVM的schedule参数,准确率超过85%,调优时间从2小时降到了8分钟。

但这个方向的最大挑战是:编译器必须深度理解模型的语义,而不是简单做IR翻译。比如,编译器要能识别出”这个attention head在训练时被剪枝过,80%的权重为零”,从而自动跳过这些零值计算。目前没有任何编译器能做到这一点,都是靠人工提供稀疏化后的矩阵格式。

第三个预测:异构计算会从多芯片走向单芯片内的多引擎协作。 昇腾的AICore架构已经展示了这种可能性:不同核心跑不同任务。但这个理念还能更进一步——在同一个芯片内集成不同类型的小型加速器,针对特定算子极端优化。

我设想的一个chiplet架构:

  • 1-2个大型矩阵乘法引擎(类似Groq LPU,纯SRAM,处理attention和MLP的密集计算)
  • 一个标量处理器集群(处理序列生成时的token解码,这部分是串行的,不需要大算力但需要低延迟)
  • 一个专用的嵌入查表加速器(高带宽、大容量,专门处理巨大的embedding table)
  • 一个通用向量处理器(处理LayerNorm、GELU这些奇形怪状的算子)

这些模块通过片上网络互联,编译器根据模型结构自动分配任务。这种架构能同时获得LPU的低延迟、GPU的通用性、TPU的训练效率。但挑战是:片上互联的设计极其复杂,功耗控制也很难。我在某个硬科技孵化器看过这类芯片的demo,性能数据很好看(能效比A100高3倍),但良率和成本完全不可控。

第四个预测:软件栈会从框架之争走向运行时之争。 现在的AI开发生态是PyTorch一统天下,但PyTorch的eager execution模式跟推理芯片的静态图需求有本质矛盾。未来的趋势是:训练继续用PyTorch的灵活性,但推理会完全走向图编译模式。

我观察到的实际动向:PyTorch 2.0的torch.compile就是朝这个方向在走。它把eager code转成静态图,然后交给各种后端(Inductor、TensorRT、OpenXLA)优化。这个架构如果成熟了,理论上可以抹平不同硬件的差异——只要你的硬件实现了标准的图执行接口。

实际上,Groq和昇腾都在往这个方向靠。Groq支持ONNX作为中间表示,昇腾的torch_npu直接集成到了torch.compile框架里。未来会出现类似”AI芯片的POSIX标准”:一套统一的图IR接口,让应用层完全不感知硬件差异。但这需要整个行业的共同努力,而且大厂(特别是NVIDIA)没什么动力这么做。

最后说一个我个人的判断:在3-5年内,GPU不会被取代,但它的市场份额会被蚕食。在特定场景下(中小模型、低延迟、高吞吐),Groq LPU这类确定性架构会成为主流。在大规模训练和多模型部署场景下,昇腾的异构架构有独特优势。而A100/H100依然会统治那些需要灵活性和生态支持的通用场景。

这个判断来自我一个惨痛的教训:去年试图把整个推理平台全迁到昇腾,结果花了6个月只完成了40%的模型迁移,剩下60%因为生态问题实在推不下去。最终采取了混部架构——延迟敏感的模型跑Groq LPU,大模型训练跑A100,多模型并发跑昇腾。虽然运维复杂度高了点,但成本降了35%,性能没打折扣。

AI推理芯片这场革命,不会是一个英雄打败恶龙的故事,而是一个生态位分化的过程。不同架构在不同场景下各自称王,工程师要做的不是选边站,而是根据业务特性搭配合适的武器。这也是我这些年踩坑无数后最深刻的领悟。

发表评论