← 返回信息流
AI 资讯Hacker News·2 小时前

Tensor芯片展现强劲实力

原标题:Tensor Is the Might

速览

Google自研Tensor芯片在AI计算任务中展现出强大性能,相比前代有显著提升。该芯片针对机器学习模型进行了优化,有望推动移动端AI应用发展。

AI 深度解读

背景

Tensor(张量)是深度学习的数学基石,从简单的两层MLP到GPT-5,所有神经网络本质上都是浮点数在操作图中流动。本文作者从零开始用C语言构建了一个完整的加速张量库,灵感主要来自Fabrice Bellard的libnc(遗憾的是该库尚未开源)。文章旨在揭示张量这一优雅数学抽象的本质:它解决了如何高效表示和操作多维数据的问题。

核心内容

张量的本质

张量本质上就是一个扁平的浮点数数组,加上一些元数据(metadata),告诉如何将这些数解释为多维对象。二维数组可以更好地表示为一维数组加上行/列数——张量本质上就是这一思想的推广。

对于超过二维的情况,需要额外的元数据,例如通用的形状(shape):

float data[32 * 3 * 28 * 28]; // 32张图片,3个通道,28x28像素
int shape[4] = {32, 3, 28, 28}; // 张量的形状
int ndim = 4; // 维度数量

有了形状,在行优先C顺序(row-major C-order,大多数现代张量库的默认布局)下,4D张量中位置[n,c,h,w]的元素位于偏移量data[n*(3*28*28)+c*(28*28)+h*28+w]

每次都这样手动计算索引效率低下,因此可以在已知形状后预计算步长(strides)。步长告诉我们在给定维度上每前进一个元素需要跳过多少个元素。

张量结构体定义

将形状相关字段组织到一起:

struct ut_shape {
    int ndim;    // 维数
    int nelem;   // 元素总数
    int shape[4];  // 形状
    int strides[4]; // 各维度的步长
};

struct ut_tensor {
    struct ut_shape shape; // 形状
    float *data;          // 数据指针
    // ... 后续会添加更多字段
};

辅助函数

创建形状和将多维索引转换为扁平索引的辅助函数:

ut_shape ut_shape_new(int ndims, int* dims) {
    ut_shape s = {.ndims = ndims, .nelems = 1};
    for (int i = 0; i < ndim; i++) {
        s.shape[i] = dims[i];
        s.nelem *= dims[i];
    }
    return s;
}

int ut_index(ut_shape s, const int* idx) {
    int flat = 0, stride = 1;
    for (int i = s.ndim - 1; i >= 0; i--) {
        flat += idx[i] * stride;
        stride *= s.shape[i];
    }
    return flat;
}

示例:ut_shape_new(3, (int[]){2, 3, 4}) 得到nelem=24,元素[1][2][3]的扁平索引为1*12 + 2*4 + 3 = 23

内存管理

张量通常动态分配,提供创建和销毁的封装(基于malloc/free)。有时需要创建共享同一数据的张量(如视图、转置、改变形状),这就要求跟踪数据所有权。为此增加引用计数(refcount)字段,以及指向所有者张量的指针:

struct ut_tensor {
    struct ut_shape shape;
    float *data;
    int refcount;          // 引用计数,减到0时释放
    struct ut_tensor *owner; // 如果此张量是视图,指向所有者
};

还可进一步使用arena分配器或内存池优化,但当前设计已是不错的起点。

逐元素操作

最基本的张量操作是逐元素(elementwise)操作——单循环遍历所有元素,对每个元素应用函数(一元)或每对元素应用函数(二元)。

一元操作示例:ew_neg, ew_relu 二元操作示例:ew_add

统一封装为:

  • static ut_tensor* ew_unary(ut_tensor* a, void (*fn)(float*, const float*, int))
  • static ut_tensor* ew_binary(ut_tensor* a, ut_tensor* b, void (*fn)(float*, const float*, const float*, int))

进而实现高层API:ut_neg, ut_exp, ut_sigmoid, ut_tanh, ut_relu, ut_add, ut_sub, ut_mul, ut_div, ut_scale

在二元操作前应assert形状相同。虽然可以支持广播(broadcasting),但作者决定暂时不做——因为目标模型中的张量形状通常完美对齐,若不齐可显式复制数据或后续添加广播索引计算。

GPU加速

单纯在CPU上运行操作很快就会遇到性能瓶颈。即使使用BLAS、LAPACK、OpenMP,CPU-only的张量性能也无法与GPU加速框架相提并论。因此需要将部分操作卸载到GPU。

难点在于GPU厂商众多,各自有不同API和特性:CUDA、OpenCL、WebGPU、Metal、Vulkan等。作者选择从Apple的Metal开始,虽然限制在Apple设备上,但Apple Silicon的统一内存(unified memory)简化了问题。

Metal使用自己的语言MSL(Metal Shading Language)定义GPU内核,类似于现代C++。内核可在运行时编译,因此可以直接在C代码中以字符串形式定义:

static const char *shaderSource =
"#include <metal_stdlib>\n"
"using namespace metal;\n"
"\n"
"kernel void relu(device const float *in [[buffer(0)]],\n"
"                  device float *out [[buffer(1)]],\n"
"                  uint id [[thread_position_in_grid]]) {\n"
"    float val = in[id];\n"
"    out[id] = fmax(val, 0.0f);\n"
"}\n";

要让内核在GPU上运行,需要创建设备(device)、构建命令队列(command queue,GPU是异步的)、编译内核、创建缓冲区等。

关键要点

  • 张量是扁平数组+形状元数据:多维数据只需一个连续内存块和描述各维度大小的shape数组即可表示,步长可预计算加速索引。
  • 行优先C顺序是默认布局:大多数现代张量库采用row-major C-order,多维索引转换为扁平索引的公式为:从最后一个维度开始累加。
  • 引用计数实现数据共享:通过refcount和owner指针支持视图、转置、形状修改而不复制数据,并允许张量在原始作用域之外存活(如迭代训练中避免重复分配)。
  • 逐元素操作是基础:所有一元和二元操作可以通过统一循环实现,对外提供简洁的API。
  • 广播支持暂不实现:当前目标模型张量形状已对齐,广播可以后续通过显式对齐或广播索引计算添加。
  • GPU加速是性能关键:即使CPU优化(BLAS、OpenMP等)也无法达到GPU性能;选择从Metal开始,利用Apple Silicon的统一内存降低复杂度。
  • Metal内核用MSL字符串定义:MSL类似现代C++,运行时编译,可直接嵌入C代码字符串。

意义与影响

本文以实战方式清晰阐明了张量抽象的核心设计原理——从结构定义、索引计算、内存管理到逐元素操作,再到GPU加速的选择。它不仅是一篇技术教程,更展示了如何从零构建一个最小可行张量库(类似微型PyTorch/TensorFlow),让读者理解现代深度学习框架底层的工作机制。对于想深入理解张量库实现(如CUDA内存管理、步长计算)的开发者,这是一份宝贵的入门指南。同时,作者对Metal的选择也反映了在Apple生态中构建高效张量运算的务实路径,为后续扩展至其他GPU API(CUDA、Vulkan等)奠定了基础。

查看原文 →zserge.com