想象你管理着一座十万平米的百货仓库:上千排货架整整齐齐,但绝大多数货位空空如也,只有几十个角落稀稀拉拉摆着商品。每次总部要盘点,如果拿着整层楼的平面图一个格子一个格子地核验,你就会把 99% 的时间浪费在“确认这里有货吗?没有”上——对着空气干活。真实世界的矩阵,绝大多数也是这种“空仓库”:推荐系统里用户与商品的交互矩阵,上亿商品中一个用户只逛过几十个;社交网络里用户好友关系矩阵,数亿用户每人只认识几百人。我们把这种“零元素占绝对多数”的矩阵称为稀疏矩阵。cuSPARSE 就是 NVIDIA 为这类“空仓库”定制的物流手账系统:它不保存任何空货位的信息,只记录“哪排哪列有货、货是什么”,让 GPU 这张超级流水线只拣有货的货架,从此告别“对着零做乘法”的尴尬。

岗位关联

掌握 cuSPARSE,你才能在推荐系统、图神经网络这类高稀疏真实场景中写出真正跑满 GPU 算力的代码——数据稀疏是常态,稠密才是特例。

目录
  1. 可视化演示
  2. 1. 稀疏世界
  3. 2. 三种格式
  4. 3. SpMV 向量
  5. 4. SpGEMM 矩阵
  6. 5. GPU 并行
  7. 6. API 流程
  8. 7. 优化策略
  9. 8. 工业应用
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态展示CSR存储格式、SpMV稀疏矩阵向量乘、2:4结构化稀疏与格式对比四个视角,直观理解GPU稀疏计算核心机制。

本节配有交互动效(进入视口后自动加载,约 3KB)。

1. 稀疏世界

假设你管理着一座十万平米的百货仓库,货架有十万个货位,但真正摆上货的只有几十个位置。每次盘点,你若拿着整层楼的平面图一个格子一个格子地核验,绝大多数时间都在对着空气确认“这里没有货”。真实世界的运算数据,很多时候就是这种空仓库:推荐系统里用户与商品的交互矩阵,上亿商品中用户只浏览过几十个;社交网络里用户的好友关系矩阵,数亿用户每人只认识几百人;自然语言处理里的词共现矩阵,词典里绝大多数词对从未同时出现。这些矩阵的共同特征——零元素占了绝对多数——就是稀疏。

如果用稠密方式存储,一个 1 亿行 × 1 亿列的交互矩阵要存 \(10^{16}\) 个元素,按 8 字节计就是 8 万万亿字节,远超任何 GPU 的显存。这就像把整层楼的平面图打印成工程蓝图,99.99% 的格子里写着一个“空”字——这张纸的费用,就是我们白白消耗的存储空间与搬运时间。GPU 的算力再强,也经不起这种“空转”。

为了量化稀疏程度,我们引入填充率(fill-in ratio)指标:

填充率只有万分之一甚至更低的矩阵,若还按稠密算法去跑,GPU 的计算单元全在做“零乘任意数”的无用功。cuSPARSE 正是为这种空仓库而生的管理系统——它只登记和搬运有货的货位,让 GPU 的每一条流水线都花在刀刃上。仓库越空,这套手账的价值就越大。

ρ=nnz(A)m×n\displaystyle \rho = \frac{\mathrm{nnz}(A)}{m \times n}
填充率表示矩阵中“有货货位”的比例。ρ 越小,矩阵越稀疏,cuSPARSE 的用武之地越大。
  1. nnz 含义nnz(A) 表示矩阵 A 中非零元素的总个数,如同仓库手账上登记的货物总件数。
    nnz(A)=∣{aij∣aij≠0}∣\mathrm{nnz}(A) = |\{a_{ij} \mid a_{ij} \neq 0\}|
  2. 总货位数m 行乘 n 列得到矩阵的全部元素个数,也就是仓库的全部货位。
    m×nm \times n
  3. 比值含义两者相除得到有货货位占比。当 ρ 小于 1%,矩阵就是典型的“空仓库”。
    ρ=nnz(A)m×n\rho = \frac{\mathrm{nnz}(A)}{m \times n}

2. 三种格式

仓库管理员不喜欢画平面图,他更愿意记手账。针对同一批货物,有三种记法:最简单的是每件货物贴一张便签,上面写着“第几排第几列 + 货名”,一叠便签就是一本清单,这叫 COO 格式;更专业的是按“排”整理成台账——先写下每一排的货物从第几号便签开始,再罗列每张便签的列号和货名,这叫 CSR 格式;还有一种按“列”整理的台账,叫 CSC 格式。cuSPARSE 三种都支持,就像集团统一配发了三种手账模板。

COO 格式用三个数组:行号数组 rows、列号数组 cols、数值数组 values,一一对应,直观得就像一叠便签。CSR 格式进一步压缩:用 row_ptr 记录每一行第一个非零元素在 values 中的起始下标,再用 col_idx 和 values 分别记录列号与数值。CSC 则是把“行”换成“列”,适合按列查询。cuSPARSE 的 API 通过 cusparseCreateCsr 等函数接收这些数组,并支持三者互转,如同把便签重新装订成行台账或列台账。

CSR 存储中,“第 i 行有哪些非零”可以用一个区间来精确描述:

三种手账各有长短:COO 记录直观但查找慢,CSR 查整排飞快但改货位麻烦,CSC 适合按列做统计。实际工程中,推荐系统嵌入表常用 CSR,图神经网络消息传递常用 COO 建图再转 CSR,cuSPARSE 提供 cusparseCreateCoo 与 cusparseCsr2coo 等转换 API,让管理员随时换手账。

Ai,j≠0  ⟺  j=col_idx[k],k∈[row_ptr[i], row_ptr[i+1])\displaystyle A_{i,j} \neq 0 \iff j = \mathrm{col\_idx}[k], \quad k \in \big[\mathrm{row\_ptr}[i],\, \mathrm{row\_ptr}[i+1]\big)
这个式子是说:第 i 行的非零元素,在 values 数组里恰好占据 row_ptr[i] 到 row_ptr[i+1]-1 这一段连续区间。
  1. 行指针 row_ptrrow_ptr[i] 存放第 i 行第一个非零元素在 values 中的下标。
    row_ptr[i]\mathrm{row\_ptr}[i]
  2. 半开区间左闭右开区间 [row_ptr[i], row_ptr[i+1]) 表示第 i 行全部非零的编号范围。
    [row_ptr[i], row_ptr[i+1])\big[\mathrm{row\_ptr}[i],\, \mathrm{row\_ptr}[i+1]\big)
  3. 列号取值范围内每个 k 对应的 col_idx[k] 就是该非零元素的列号。
    col_idx[k]\mathrm{col\_idx}[k]

3. SpMV 向量

总部要求:每个排口统计货物总额,每件货物的“价值”等于毛重乘以系数。仓库管理员翻开 CSR 台账,从第一排第一件开始,找到列号、取出系数、乘完累加,逐件处理,完全不用去空货架上逛一圈。这个动作在数学上叫 SpMV(稀疏矩阵乘向量):给定稀疏矩阵 A 和稠密向量 x,计算结果向量 y = Ax。

CSR 格式下的 SpMV 可以精确写成求和式:第 i 行的结果,等于该行所有非零元素 values[k] 与向量 x 中对应列号取值 x[col_idx[k]] 的乘积之和。

GPU 上实现 SpMV 时,最自然的做法是每个线程负责一行:线程 t 只翻第 t 排的台账,逐件乘加。稠密算法算 100 个货位的排要 100 次乘法,稀疏手账可能只需要 3 次——省下的不仅是乘法,更是从显存取回大量零元素的内存带宽。这就是 SpMV 在推荐系统、科学计算迭代求解器中被反复调用的根本原因。

yi=∑k=row_ptr[i]row_ptr[i+1]−1values[k]⋅x[col_idx[k]]\displaystyle y_i = \sum_{k = \mathrm{row\_ptr}[i]}^{\mathrm{row\_ptr}[i+1] - 1} \mathrm{values}[k] \cdot x\big[\mathrm{col\_idx}[k]\big]
稀疏计算的精髓就在这个公式里:求和只遍历有货货位,零元素根本不出现。
  1. 行下标 iy 的第 i 行由矩阵 A 的第 i 行决定,对应仓库第 i 排货架的统计任务。
    yiy_i
  2. 求和区间k 从 row_ptr[i] 到 row_ptr[i+1]-1,恰好是台账上第 i 排的货物编号范围。
    ∑k=row_ptr[i]row_ptr[i+1]−1\sum_{k = \mathrm{row\_ptr}[i]}^{\mathrm{row\_ptr}[i+1] - 1}
  3. 取数相乘values[k] 是货物毛重,x[col_idx[k]] 是按列号从系数表 x 中取出的对应系数。
    values[k]⋅x[col_idx[k]]\mathrm{values}[k] \cdot x[\mathrm{col\_idx}[k]]

4. SpGEMM 矩阵

公司并购了隔壁仓库,要把两家货合并统计“组合价值”:A 仓库第 k 列有货、B 仓库第 k 行也有货,两件货才能组合成一件新产品。这比单排拣货复杂得多——需要同时翻两本手账,找“列号”和“行号”都能对上的货位。这个操作叫 SpGEMM(稀疏矩阵乘稀疏矩阵),计算 C = A × B。

对于 C 的每个元素,必须满足“中间维度 k 在 A 的第 i 行非零、同时在 B 的第 k 行非零”,才有一个乘积项需要累加:

SpGEMM 是稀疏计算中最难啃的骨头:组合后的仓库可能比原来任何一个都拥挤(中间结果膨胀),也可能更空(大量乘积对相互抵消)。更麻烦的是,输出矩阵 C 的稀疏度在计算前难以预测,需要先扫一遍估算非零数量、做好内存预算,再正式计算。cuSPARSE 提供 cusparseSpGEMM 的两阶段接口(估算 + 执行),正是为了解决“新仓库到底要准备多少货架”的问题。

Ci,j=∑k: Ai,k≠0  ∧  Bk,j≠0Ai,k⋅Bk,j\displaystyle C_{i,j} = \sum_{k:\, A_{i,k} \neq 0 \;\land\; B_{k,j} \neq 0} A_{i,k} \cdot B_{k,j}
这个式子的关键在于求和条件:不是所有 k 都参与计算,只有“两边都有货”的 k 才产生贡献。
  1. 乘积项条件A_{i,k} 非零且 B_{k,j} 非零,两个条件同时满足才有一个非零的中间乘积。
    Ai,k≠0  ∧  Bk,j≠0A_{i,k} \neq 0 \;\land\; B_{k,j} \neq 0
  2. 只累加非零对求和范围被限制在满足条件的 k 上,跳过任何一边为空货位的组合。
    ∑k: Ai,k≠0  ∧  Bk,j≠0\sum_{k:\, A_{i,k} \neq 0 \;\land\; B_{k,j} \neq 0}
  3. 乘积累加把 A_{i,k} 与 B_{k,j} 的乘积累加到 C_{i,j} 中。
    Ai,k⋅Bk,jA_{i,k} \cdot B_{k,j}

5. GPU 并行

新仓库请了几百个搬货工人,每个工人负责一条货架排。问题立刻暴露:第 3 排有 50 件货,第 7 排只有 1 件货,干完活的工人只能站在一旁等待,负责 3 排的工人累得满头大汗。GPU 上跑 SpMV 也是同一个道理:每个线程处理一行,行非零数量差异悬殊时,整个 warp(32 个线程的小组)的执行时间被最重的那一行拖死。

衡量这种不均衡程度,我们定义一个负载不均衡度 SU(Skewness of Utilization):

除了负载不均衡,GPU 访存还有两个坑:合并访问要求相邻线程读相邻内存,否则一次内存事务只搬运一个有用数据;bank 冲突则发生在 warp 内多个线程同时访问共享内存的同一分区,如同多个工人抢同一个货梯。cuSPARSE 在选择内部算法时,会根据稀疏模式自适应地切换到行并行、非零并行或分段扫描等策略,尽量让每个线程的货量均匀、访存连续。

SU=max⁡i  nnz(Ai:)1m∑i=1mnnz(Ai:)\displaystyle \mathrm{SU} = \frac{\max_{i} \; \mathrm{nnz}(A_{i:})}{\dfrac{1}{m}\sum_{i=1}^{m} \mathrm{nnz}(A_{i:})}
SU 越大,说明各行工作量差异越悬殊,GPU 的空转越严重。理想情况下 SU 接近 1。
  1. 分子所有行中非零数量最多的那一行的工作量,相当于最重的一排货架。
    max⁡i  nnz(Ai:)\max_{i} \; \mathrm{nnz}(A_{i:})
  2. 分母所有行非零数量的平均值,相当于全仓库的平均货量。
    1m∑i=1mnnz(Ai:)\frac{1}{m}\sum_{i=1}^{m} \mathrm{nnz}(A_{i:})
  3. 比值解释两者相除得到“最忙线程相对平均水平的倍数”,倍数越大,越多人干等。
    SU\mathrm{SU}

6. API 流程

仓库引入了标准化作业流程,cuSPARSE 的编程套路也像一张标准作业单:先领一个工作许可证,再提交仓库结构图,然后备好货物清单,最后喊一句通用口令干活。具体来说,第一步 cusparseCreate 创建句柄 handle 作为许可证;第二步用 cusparseCreateCsr 创建矩阵描述符,把 CSR 的三个数组交给库;第三步是执行运算,比如 cusparseSpMV;第四步收工后 cusparseDestroy 销毁句柄和描述符。

cuSPARSE 提供的通用 SpMV 语义比裸的 y=Ax 更灵活,支持缩放和转置:

这套流程与 cuBLAS 的句柄设计如出一辙:handle 管理上下文,描述符描述数据布局,alpha/beta 控制累积语义。学会 cuSPARSE 之后再学 cuDNN/cuBLAS,会发现 API 风格高度统一,如同集团旗下所有仓库用同一套作业标准书。

y←α⋅op(A) x+β⋅y\displaystyle y \leftarrow \alpha \cdot \mathrm{op}(A)\, x + \beta \cdot y
这个公式是 cuSPARSE/cuBLAS 通用的“带参线性运算”约定:对 A 做可选转置(op),再乘以标量 α,加上标量 β 与 y 的乘积。
  1. op 转置op(A) 表示对 A 做原样(N)、转置(T)或共轭转置(H),如同按“排”或按“列”翻台账。
    op(A)\mathrm{op}(A)
  2. α 缩放整个乘积乘以系数 α,实现 y = αAx 的缩放。
    α⋅op(A) x\alpha \cdot \mathrm{op}(A)\, x
  3. β 累加加上 β 倍的旧 y,支持原地累积计算,避免反复分配临时数组。
    +  β⋅y+ \;\beta \cdot y

7. 优化策略

干了二十年的老仓管员有三条秘诀:第一,提前把台账本订够页数,别等记到一半发现没纸了——对应 内存预分配,cuSPARSE 的 SpGEMM 先用估算阶段算出输出非零数量,一次性分配好显存;第二,把货物按列号重新排队,让同一趟送货路线上的货位挨在一起——对应 非零排序与重排,提高缓存命中率;第三,换一把更快的电子秤——对应 混合精度,在不损失精度的场景用 FP16 替换 FP32,让 Tensor Core 跑得更快。

存储格式本身也值得优化。CSR 的压缩效果可以用压缩比来量化:稠密存储需要 mn 个元素,而 CSR 需要 row_ptr(m+1 个)、col_idx(nnz 个)、values(nnz 个):

最后的优化是按场景选格式:行访问密集选 CSR,列访问密集选 CSC,图动态建边选 COO,结构化稀疏(如 2:4 剪枝)用 cuSPARSE 的块稀疏格式配合 Tensor Core。优化没有银弹——本质是让 GPU 的缓存、访存单元和流水线都尽量满负荷运转。

η=m×n2×nnz+m+1\displaystyle \eta = \frac{m \times n}{2 \times \mathrm{nnz} + m + 1}
压缩比 η 表示稀疏存储省了多少倍空间。η 越大,说明手账相比平面图的优越性越明显。
  1. 总货位分子 m×n 是稠密矩阵的全部元素个数,即平面图上的所有格子。
    m×nm \times n
  2. 台账开销分母 2nnz+m+1 是 CSV 三个数组的总长度:values 和 col_idx 各 nnz 个,row_ptr 有 m+1 个。
    2×nnz+m+12 \times \mathrm{nnz} + m + 1
  3. 压缩倍数两者相除得到压缩比,例如 η=1000 表示手账只用了平面图千分之一的纸。
    η=m×n2×nnz+m+1\eta = \frac{m \times n}{2 \times \mathrm{nnz} + m + 1}

8. 工业应用

这套“仓库手账”如今是全集团的标配。在推荐系统里,用户-物品交互矩阵就是一座巨型空仓库,ctr 预估模型需要查询用户点击过的几百个物品的嵌入向量——这正是 SpMV:用稀疏交互矩阵乘嵌入矩阵,只取非零位置的嵌入。在图神经网络中,社交网络的邻接矩阵同样极度稀疏,GCN 的每一层都在做“邻居消息聚合”,本质就是一个稀疏矩阵乘特征矩阵(SpMM),cuSPARSE 让亿级节点的图也能在 GPU 上训练。

GNN 消息传递的数学形式,与 SpMM 完全对应:

科学计算同样是 cuSPARSE 的主战场:有限元分析、天气预报、油气勘探都要反复求解大规模稀疏线性方程组,每一次 Krylov 迭代都调用一次 SpMV。cuSPARSE 与 cuDNN/cuBLAS 协同工作,前者管稀疏、后者管稠密,共同支撑起从深度学习训练到科学模拟的完整 GPU 生态。稀疏不是特例,而是 AI 处理真实世界数据的默认姿势。

hi(l+1)=σ(∑j∈N(i)W hj(l))\displaystyle h_i^{(l+1)} = \sigma\left(\sum_{j \in \mathcal{N}(i)} W\, h_j^{(l)}\right)
每个节点 i 的新特征,等于所有邻居 j 的特征经过权重 W 变换后求和。邻居集合 N(i) 正是邻接矩阵第 i 行的非零列——稀疏的典型形态。
  1. 邻居集合N(i) 是节点 i 的邻居,对应稀疏邻接矩阵第 i 行所有非零位置。
    N(i)\mathcal{N}(i)
  2. 求和聚合对每个邻居变换后的特征求和,累加顺序与 CSR 存储的求和区间完全一致。
    ∑j∈N(i)W hj(l)\sum_{j \in \mathcal{N}(i)} W\, h_j^{(l)}
  3. 激活函数σ 是激活函数,例如 ReLU,相当于对统计结果做一次“审批盖章”。
    σ(⋅)\sigma(\cdot)

AI 应用场景

推荐系统 Embedded 查询

阶段:特征工程与模型推理

解决的问题:传统方式把用户-物品交互矩阵当稠密矩阵处理,上亿商品对每个用户都做全量打分,计算量和显存都不可承受。利用 cuSPARSE 的 SpMV,只看用户实际交互过的几十个商品,把嵌入表查询变成稀疏矩阵乘向量,计算量直降几个数量级。

具体案例:某短视频推荐平台的双塔模型维护一张 10 亿 × 128 的嵌入表,用户交互序列构成稀疏矩阵。线上推理时用 cuSPARSE 的 cusparseSpMV 对交互矩阵与嵌入表做稀疏乘,单次请求嵌入查询延迟从毫秒级降到亚毫秒级,GPU 利用率提升 3 倍以上。

图神经网络消息传递

阶段:模型训练与推理

解决的问题:GCN/GAT 每层都要做邻居特征聚合,邻接矩阵稀疏度常超过 99.99%。朴素实现按稠密矩阵乘,显存和算力都被零元素浪费。cuSPARSE 的 SpMM 把聚合计算严格限定在邻接矩阵非零位置,让亿级节点图训练成为可能。

具体案例:某社交平台的欺诈检测系统构建 3 亿节点、30 亿边的用户关系图,GCN 训练时用 cuSPARSE 的 cusparseSpMM 做消息聚合,相比稠密实现显存占用降到 1/50,单轮训练时间从小时级压缩到分钟级。

科学计算迭代求解器

阶段:数值模拟与仿真

解决的问题:有限元分析、流体仿真中的偏微分方程离散后产生巨型稀疏方程组,每次迭代都要做 SpMV。传统 CPU 实现受内存带宽限制,cuSPARSE 让迭代求解器的主循环直接在 GPU 上运行,配合预条件子大幅加速。

具体案例:某汽车企业的碰撞仿真中,网格矩阵规模 5000 万×5000 万、非零约 3 亿。使用 cuSPARSE 的 SpMV 驱动共轭梯度法,单次迭代时间从 120ms 降到 9ms,整场仿真提速 13 倍,工程师能在一晚完成原本一周的迭代设计。

Python 演示

python46 行
# -*- coding: utf-8 -*-"""CSR 稀疏矩阵与 SpMV 的极简实现, 提前演练 cuSPARSE 的核心逻辑""" import numpy as np  def dense_to_csr(A):    """把稠密矩阵 A 压缩成 CSR 三个数组: row_ptr, col_idx, values"""    m, n = A.shape    row_ptr = [0]    col_idx = []    values = []    for i in range(m):        for j in range(n):            if A[i, j] != 0:                col_idx.append(j)                values.append(A[i, j])        row_ptr.append(len(values))    return (np.array(row_ptr, dtype=np.int32),            np.array(col_idx, dtype=np.int32),            np.array(values, dtype=np.float64))  def csr_spmv(row_ptr, col_idx, values, x):    """y = A * x, 按 CSR 手账逐行逐件拣货, 跳过所有空货位"""    m = len(row_ptr) - 1    y = np.zeros(m)    for i in range(m):        for k in range(row_ptr[i], row_ptr[i + 1]):            y[i] += values[k] * x[col_idx[k]]    return y  if __name__ == "__main__":    A = np.array([[0, 0, 5, 0],                  [8, 0, 0, 0],                  [0, 0, 0, 3],                  [0, 6, 0, 0]], dtype=np.float64)    x = np.array([1.0, 2.0, 3.0, 4.0])    row_ptr, col_idx, values = dense_to_csr(A)    print("row_ptr =", row_ptr)    print("col_idx =", col_idx)    print("values  =", values)    y = csr_spmv(row_ptr, col_idx, values, x)    print("y =", y)    print("numpy 校验:", A @ x)
目的:手写一个 CSR 压缩与 SpMV,完整模拟 cuSPARSE 在 GPU 上做“只算有货货位”的核心流程,并和 numpy 稠密结果对账。
  1. if A[i, j] != 0:跳过空货位,只把有货货位的列号和值收进手账。
  2. row_ptr.append(len(values))记下每一排第一件货的编号,这是 CSR 的灵魂。
  3. for k in range(row_ptr[i], row_ptr[i + 1]):只遍历第 i 排台账上的货物,空货位根本不出现。
  4. y[i] += values[k] * x[col_idx[k]]用列号 col_idx[k] 从向量 x 里取系数,乘完累加。
  5. print("numpy 校验:", A @ x)与稠密矩阵乘法结果对账,验证手账没算错。

常见误区

误区cuSPARSE 既然叫“稀疏库”,那我只要把完整的稠密矩阵交给它,它自动就知道跳过零元素了吧?我的代码里已经有个 numpy 数组了,直接传进去不就行了?

正解cuSPARSE 要求你必须先完成稀疏压缩——显式提供 values、col_idx、row_ptr 三个数组,并通过 cusparseCreateCsr 描述稀疏结构。它看到的只有压缩后的非零数据,根本没有“完整矩阵”的概念。就像仓库手账只记录有货货位,库管员不会拿到整层楼的平面图再自己找货。如果传输稠密矩阵,显存带宽在数据搬运阶段就已经被零元素耗尽了。

为何常见:cuBLAS 等稠密库都是直接接收完整数据,初学者容易把 cuSPARSE 想成“自动压缩”的魔法盒,忽略了稀疏格式是使用的前提而非库的附加能力。

误区我听说稀疏计算性能提升巨大,那是不是只要矩阵里有零,用 cuSPARSE 就一定比稠密计算快?那我把所有矩阵都声明成稀疏的好了。

正解当填充率较高(比如超过 30%)时,稀疏格式的索引访问、分支判断、随机访存开销会超过省下的乘法。此时 CSR 的 col_idx 间接寻址反而让 GPU 访存变得支离破碎,稠密计算配合共享内存的矩阵分块反而更快。判断标准是“空仓库值得不值得换手账”——货位利用率高了,平面图直接看反而方便。

为何常见:宣传材料总强调“稀疏加速 XX 倍”,让人误以为稀疏格式天生优越。实际上 cuSPARSE 内部也有稠密回退策略,高填充率下它自己都会建议用 cuBLAS。

误区我学会了 CSR 格式,那以后所有稀疏矩阵都统一用 CSR 存储吧,反正 cuSPARSE 支持它,一劳永逸。

正解CSR 优化的是“按行访问”:逐行遍历、行的连续区间都在内存中挨着。但如果你需要频繁按列操作——比如计算每列的和、提取某一列做特征——CSC 才是正确选择。COO 在动态建图(频繁插入新边)时最方便,因为只需 append 新便签;而图神经网络常用 COO 建图再转 CSR 训练。cuSPARSE 提供格式互转 API,正式原因就是没有万能格式。

为何常见:教程通常拿 CSR 做最详细的讲解,学习者产生“最重要的就是最好的”的错觉,忽略了格式设计与访问模式强相关的本质。

误区SpGEMM 不就是把稠密矩阵乘法的循环里加个 if 判断,遇到零就跳过吗?流程完全一样,写起来也不难。

正解稠密矩阵乘法 C = A×B 的三重循环是固定执行计划;SpGEMM 必须先做“行-列非零匹配”——A 的第 i 行非零列号集合与 B 的第 j 行非零行号集合取交集,交集中的 k 才参与计算。更麻烦的是输出 C 的稀疏模式未知:乘积可能累加成新的非零,也可能相互抵消,需要两阶段(估算+执行)或动态内存管理。这是 cuSPARSE 提供 cusparseSpGEMM 而不是简单封装的原因。

为何常见:许多人把“稀疏矩阵乘法”理解成“能跳过零的稠密乘法”,低估了输出稀疏度未知带来的内存与调度复杂度。

误区填充率从 1% 降到 0.1%,非零少了十倍,那 SpMV 应该也快十倍吧?GPU 不就是靠省乘法来加速的?

正解GPU 性能远非线性地取决于计算量。非零少但分布不均时,负载不均衡让大量线程空转;非零列号跳跃时,x[col_idx[k]] 的随机访存让缓存命中率暴跌;共享内存上的 bank 冲突进一步放大损失。cuSPARSE 的优化策略是排序重排、行合并、分块稀疏等手段,改善的是访存与负载模式,而非单纯减少计算。手账记的货少了,但不代表送货路线一定更顺。

为何常见:CPU 上的稀疏加速主要来自省计算量,人们把 CPU 经验迁移到 GPU,忽视了带宽、合并访问、线程调度这些 GPU 特有的性能瓶颈。

面试题

Q1. CSR 格式中 row_ptr 数组的作用是?

单选题 · easy

  • A.记录每行非零元素的起始位置← 正确答案
  • B.记录每列非零元素的个数
  • C.存储矩阵全部的行列数值
  • D.标识矩阵的行数与列数

答案:A。row_ptr[i] 表示第 i 行第一个非零元素在 values/col_idx 中的下标,row_ptr[i+1]-row_ptr[i] 正是该行非零个数。它如同仓库台账上“每排货物从第几号便签开始”的索引,记录的是行区间起点。B 混淆了 CSR 与 CSC,C 混淆了 values 与 col_idx 的功能,D 是矩阵形状信息而非存储索引。

Q2. 填充率 ρ 的定义是?

单选题 · easy

  • A.非零元素数与总元素数的比值← 正确答案
  • B.矩阵行数与列数的比值
  • C.非零行数与总行数的比值
  • D.已用显存与总显存的比值

答案:A。填充率 ρ = nnz(A) / (m×n),衡量矩阵中有货货位占总货位的比例。B 是矩阵宽高比,C 只统计了含非零的行,忽略了行内零元素,D 是显存使用率,都与稀疏度的定义无关。ρ 越小,cuSPARSE 手账的价值越大。

Q3. 一个 1 亿行 × 1 亿列的稠密矩阵按 8 字节存储,需要多少空间?

单选题 · easy

  • A.8×10^16 字节← 正确答案
  • B.1×10^16 字节
  • C.8×10^8 字节
  • D.4×10^12 字节

答案:A。1 亿×1 亿 = 10^8×10^8 = 10^16 个元素,每个元素 8 字节,共 8×10^16 字节,远超任何 GPU 显存。这正是“空仓库平面图”的浪费:明明没有货,图纸上的空格子却占了全部空间。其他选项要么只算了元素个数没乘 8,要么位数错误。

Q4. SpMV 中访问稠密向量 x 的方式是?

单选题 · mid

  • A.按 col_idx 给出的列号取值← 正确答案
  • B.顺序遍历 x 的全部元素
  • C.按 row_idx 分组取出
  • D.完全不访问向量 x

答案:A。CSR 格式下 SpMV 的核心是 values[k] × x[col_idx[k]],col_idx[k] 记录了货位列号,线程必须按这个列号去 x 中取系数,属于间接随机访问。B 是稠密乘法风格,C 混淆了行与列,D 显然错误——乘积少不了一方因子。

Q5. 下面哪个场景最适合使用 cuSPARSE?

单选题 · mid

  • A.用户物品交互矩阵的嵌入查询← 正确答案
  • B.全连接层的稠密矩阵乘法
  • C.卷积层的特征图池化
  • D.向量之间的余弦相似度

答案:A。用户-物品交互矩阵极度稀疏,嵌入查询本质是 SpMV/SpMM,是 cuSPARSE 的主场。全连接层是稠密矩阵乘法,用 cuBLAS;池化是规约,余弦相似度是向量运算,都不涉及稀疏存储结构。就像手账只适用于货位极空的仓库,满仓时直接用平面图。

Q6. SpGEMM 与稠密矩阵乘法最关键的区别是?

单选题 · mid

  • A.需要行列非零匹配及中间累积← 正确答案
  • B.乘法执行顺序完全相同
  • C.无需考虑输出稀疏度变化
  • D.只能使用单精度浮点数

答案:A。SpGEMM 要求 A 的第 i 行非零列号与 B 的第 j 行非零行号求交集,交集外的 k 直接跳过,且输出稀疏度未知需要两阶段处理。B 忽略了匹配与剪枝流程,C 恰恰相反——输出稀疏度是最大难点,D 无中生有,cuSPARSE 支持多精度。

Q7. 关于 SpGEMM 的中间结果,下列说法正确的是?

单选题 · hard

  • A.可能膨胀出大量冗余项需要后续压缩← 正确答案
  • B.会在 GPU 显存中被自动压缩存储
  • C.被 cuSPARSE 强制转换为稠密矩阵
  • D.因为稀疏度为零而完全消失

答案:A。A 的非零匹配会产生大量中间乘积,这些乘积累加后可能合并为更少输出,也可能保持膨胀状态,因此需要估算阶段预留内存,这是 SpGEMM 的经典挑战。B 没有“自动压缩”,C 会摧毁稀疏优势,D 与事实相反——中间结果通常非零且不可忽略。

Q8. GPU 上 SpMV 负载不均衡的主要原因是?

单选题 · hard

  • A.各行非零元素数量差异悬殊← 正确答案
  • B.矩阵总行数超过线程总数
  • C.非零元素的数值类型不同
  • D.GPU 核心的时钟频率不同

答案:A。行并行 SpMV 中每个线程负责一行,行间非零数量差异大(如一行 50 个非零、一行 1 个)时,最重的线程决定 warp 的执行时间,其他人干等。B 只是任务分批问题,C 数值类型影响的是算力而非均衡,D 硬件频率相同且已由调度器管理。

Q9. cuSPARSE 使用的基本流程包含哪些步骤?

多选题 · mid

  • A.cusparseCreate 建立句柄← 正确答案
  • B.cusparseCreateCsr 描述矩阵← 正确答案
  • C.cusparseSpMV 执行乘加← 正确答案
  • D.cudaFree 释放全部显存

答案:ABC。标准流程是“领证、画图、干活、销证”:Create 建句柄,CreateCsr 描述 CSR 结构,SpMV 执行 y=αop(A)x+βy。D 错误——释放句柄和描述符应使用 cusparseDestroy 系列函数,cudaFree 只释放用户自己分配的数据数组,且不在固定流程内。

Q10. 影响稀疏矩阵 GPU 计算效率的因素有哪些?

多选题 · hard

  • A.非零元素在行间的分布← 正确答案
  • B.内存访问是否合并连续← 正确答案
  • C.warp 内线程是否发生 bank 冲突← 正确答案
  • D.非零元素数值的大小

答案:ABC。行间分布决定负载均衡,访问连续性决定合并访问效率,bank 冲突影响共享内存吞吐,三者是 GPU 稀疏性能的关键。数值大小只影响浮点运算本身,不影响存储布局与访存模式,因此 D 不是效率因素。优化策略——排序、重排、分段扫描——都是在改善 A/B/C 这三个维度。