想象你是一家大型中央厨房的调度主管,每天有几百批食材从后门运来,要整齐地码进仓库货架。每批食材都是一个“四维箱子”:批次、品类、行、列。货架只有一条笔直的通道,你决定箱子内部按什么顺序摆放。麻烦的是,每位师傅都有强迫症——切配师傅喜欢按“品类”连续取料,炒菜师傅喜欢按“位置”连续取料,甜品师傅又另有一套。如果你随便摆,师傅开工前就得让搬运工把整批货重新码一遍。几十个流水步骤下来,搬运工累瘫,出菜速度也完蛋。聪明的调度主管会在进货前就规划好每个箱子的摆放方式,让师傅们随手就能拿到料,搬运工只在万不得已时才出马。这就是“布局优化”。 在 AI 编译器里,张量就是那批“四维箱子”,算子是师傅,内存是货架,而“摆放方式”就是布局(layout)。布局优化是编译器中后期对计算图做的一个关键动作:通过给每个算子提前选好输入输出的内存排列,消除大量运行时数据重排,让模型跑得更快。本文从零开始,用仓库管理的故事,把布局优化的原理、算法、工程实践一次讲透。

岗位关联

掌握布局优化,你就握住了 AI 编译器性能调优的一把关键钥匙。面试时能讲清 NCHW 与 NHWC 的区别和编译器的取舍逻辑,工作中能轻松让模型训练与推理提速 20% 以上。

目录
  1. 可视化演示
  2. 布局基础概念
  3. 常见布局格式
  4. 布局传播机制
  5. 转换代价分析
  6. 编译器实践
  7. 布局优化算法
  8. 性能影响分析
  9. 工程实践调优
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示NCHW与NHWC两种4D张量数据布局的内存排列差异与效率、布局推断传播与转换消除三阶段优化流程、以及布局优化对transpose消除和内存访问效率的提升。

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

NCHW与NHWC内存布局 NCHW通道优先为PyTorch默认布局 NHWC通道最后利于GPU访存优化 NCHW↔NHWC 按硬件特性选择最优布局

布局基础概念

小算第一次管理仓库时,面对四面货架不知所措。他决定先制定“摆放规则”:每个四维箱子(批次 N、品类 C、行 H、列 W)打开后,从里到外一层层摆。先摆完最里层的行,再摆这一行的下一行,摆完一个品类再摆下一个品类。这种从内到外的顺序,就是计算机里的“行优先”(row-major):最内层的维度变化最快,相邻元素在内存里紧挨着。

在 AI 编译器眼中,张量就是多维数组,而内存是一条连续的一维空间。如何把多维下标映射到一维偏移量,完全由“步长”(stride)决定。行优先下,最后一维步长为 1,其余维度的步长等于它右侧所有维度尺寸的乘积。这个公式虽然简单,却是后续所有布局讨论的基石。

有了步长,访问任意元素都变成一次乘法加加法。比如 NCHW 布局的 (N,C,H,W) 张量,元素 (n,c,h,w) 的偏移是 n×(C×H×W) + c×(H×W) + h×W + w。注意,如果只改步长而不搬数据,你得到的是一个“视图”;真正把数据按新顺序重排,才产生新的内存排列——后者才是布局转换。

小算把这张“维度-跨度对照表”贴在仓库柱子上,每个师傅来取货都能自己算出位置。这张表,就是张量的 strides 元数据。理解了它,后面所有布局话题都有了抓手。

stride(d)=∏k=d+1D−1size(k)\displaystyle \text{stride}(d) = \prod_{k=d+1}^{D-1} \text{size}(k)
通俗说明:某一维的步长,等于它右侧所有维度尺寸的乘积;最内层维度的步长永远是 1。
  1. 从最后一维起算最内层维度(d = D−1)的步长恒为 1,因为相邻元素在内存中直接挨着。
    stride(D−1)=1\displaystyle \text{stride}(D-1) = 1
  2. 向右连乘第 d 维的步长是右侧全部维度尺寸的连乘积,式子中的连乘符号 \prod 表示把 size 从 d+1 一直乘到 D−1。
    stride(d)=size(d+1)×size(d+2)×⋯×size(D−1)\displaystyle \text{stride}(d) = \text{size}(d+1) \times \text{size}(d+2) \times \cdots \times \text{size}(D-1)

常见布局格式

切配师傅崔姐喜欢“按品类取料”——她要一口气把 32 个通道同一位置的数据抓齐,如果货架上这些数据隔了十万八千里,她就得来回跑。炒菜师傅贺哥则喜欢“按位置取料”——他卷积时要连续滑动窗口,希望同一位置的通道挨在一起。于是仓库衍生出两种主流摆放方案。

NCHW 把批次作大区、通道作中区、高宽作小区,同一通道的数据连续存放;NHWC 把通道变成最内层,同一空间位置 (h,w) 的所有通道紧挨着。两种布局数学上完全等价,但内存访问模式截然不同。下面两个公式分别给出各自的偏移量计算方式。

GPU 的卷积核通常对 NHWC 更友好——它把 C 放在最内层,配合 SIMD 一条指令可以加载多个通道的同一像素;而 CPU 上许多算子对 NCHW 支持更成熟,因为通道维连续访问利于缓存。还有更激进的分块布局(如 NCHW_VECT_C),把通道切成 4 或 8 的向量块,让向量化更进一步。没有绝对好坏,只有合不合适。

小算在仓库里划了 A 区和 B 区,分别按两种规则摆货。他没有私心——哪个区给哪个师傅用,全看师傅的需求。这就是编译器里的“布局分配”:先了解每一位师傅的偏好,再决定货架的摆法。

offsetNCHW(n,c,h,w)=n×(C×H×W)+c×(H×W)+h×W+w\displaystyle \text{offset}_{NCHW}(n,c,h,w) = n \times (C \times H \times W) + c \times (H \times W) + h \times W + w
NHWC 中通道维最靠内,同一空间位置的多通道数据连续存放。
  1. 批次定位每批占用 H×W×C 个元素,批次 n 的起始偏移由 n 乘上它。
    n×H×W×C\displaystyle n \times H \times W \times C
  2. 行定位第 h 行的起点是 h 乘一行宽度 W×C。
    h×W×C\displaystyle h \times W \times C
  3. 列与通道定位第 w 列起点乘 C,最后加上通道号 c(取值 0 到 C−1)。
    w×C+c\displaystyle w \times C + c

布局传播机制

仓库有条不成文的规矩:上一道工序的摆放方式,最好就是下一道工序的取料方式。如果每一站都顺手,整条流水线就丝滑;如果哪一站不顺手,搬运工就要插进来重新码货。小算要做的,就是沿着流水线把所有“顺手”关系串起来。

编译器把这套“顺手”翻译成“布局约束”:每个算子在实现时,能高效处理的布局是固定的。编译器需要给图中每条边分配一个布局,使它同时满足上下游两个算子的要求。这个分配过程就叫布局传播(layout assignment)。形式化地说,对图中任意一条数据边,两端的布局必须一致:

实际编译器通常从输出端开始回溯:输出布局是确定的(比如 Softmax 要求 NHWC),往前推,Softmax 的输入最好也是 NHWC;再往前,全连接层的输入输出布局关系又形成新的约束。遇到一个有明确要求的算子,约束就“卡”住一种布局;卡不住的地方,就放一个转换节点。

小算在流水线示意图上画箭头,从最后一道菜往前推,逐站确认“这里顺不顺”。一旦发现某站不顺,他就在图上画个红圈——那里要安排搬运工。布局传播的本质,就是提前把所有红圈找出来。

Layout(uout)=Layout(vin),∀(u,v)∈E\displaystyle \text{Layout}(u_{\text{out}}) = \text{Layout}(v_{\text{in}}), \quad \forall (u, v) \in E
每条边上下游的布局必须对齐,否则就要插入转换节点。
  1. 边的两端对图中任意一条边 (u,v),u 的输出布局必须等于 v 的输入布局,否则 v 无法直接开工。
    Lu,out=Lv,in\displaystyle L_{u,\text{out}} = L_{v,\text{in}}
  2. 全图量化\forall 表示所有边都受这个等式约束,这是布局传播的“规矩”。
    ∀(u,v)∈E\displaystyle \forall (u,v) \in E

转换代价分析

搬运工老张最怕的不是箱子重,而是“整批重码”:把 NCHW 摆的货全部挪成 NHWC,等于把仓库 A 区的货全部搬到 B 区再换顺序。耗时永远和货物总量成正比,而且搬完还要对账。

布局转换(layout convert)在计算图里就是一个节点,内部实现是 transpose + copy。transpose 改变逻辑维度顺序,copy 把数据按新顺序写回。对一个 (N,C,H,W) 张量,一次完整的 NCHW→NHWC 转换的代价大约是两倍的张量元素数——读一遍、写一遍。

更麻烦的是,转换会切断算子的融合机会。编译器本来可以让 Conv+BN+ReLU 融合成一个算子,中间结果留在寄存器或缓存里;一旦插入转换,中间结果被迫写回内存,融合链断掉,性能雪上加霜。所以优化时宁可调整整个图的布局,也不轻易插入转换。

小算把老张的“重码成本”折算成表格,每次规划布局时先问一句:这里要让老张干活吗?老张一歇,流水线就快了。真正优秀的调度,是让搬运工从开工到收工都不用出手。

cost(convert)=2×N×C×H×W\displaystyle \text{cost}(\text{convert}) = 2 \times N \times C \times H \times W
一次布局转换的代价近似于“读一遍 + 写一遍”所有元素,与元素总量成正比。
  1. 读旧布局把 N×C×H×W 个元素按原布局全部读出来。
    N×C×H×W\displaystyle N \times C \times H \times W
  2. 写新布局再按新布局全部写回内存,总共发生两次访存,所以乘以 2。
    2×N×C×H×W\displaystyle 2 \times N \times C \times H \times W

编译器实践

每家中央厨房的调度制度不同。XLA 厨房派了专门的“布局分配专员”;PyTorch 厨房给师傅们发了“备选工具包”;TVM 厨房则由老师傅凭经验现场拍板。三种制度各有各的智慧,也有共同的追求。

XLA 在 HLO 优化阶段有一个专门的 LayoutAssignment pass,它会根据每个算子的后端实现,为每个值分配布局,并把用户指定的布局约束尽量推满全图。PyTorch 通过 channels_last 内存格式让模型按 NHWC 运行,torch.compile 的 Inductor 后端会自动处理布局的选择与转换。TVM 则使用 ALT(AutoTVM-based Layout Transformation),按算子类型挑选最优输入布局。它们的共同目标可以用一个优化式表达:

实践中,布局分配还会考虑“哪个算子最贵就优先迁就谁”。比如卷积占全图 90% 算力,那全图都往卷积的偏好布局上靠,即使其他小算子多几次转换也划算。这背后是一个带权重的全局优化决策。

小算听完三家厨房的汇报,得出结论:好制度不是消灭搬运工,而是让搬运工出现在最便宜的位置。编译器们比来比去,比的都是这句话。

min⁡L∑e∈E1[Lein≠Leout]⋅coste\displaystyle \min_{L} \sum_{e \in E} \mathbf{1}\left[L_e^{\text{in}} \neq L_e^{\text{out}}\right] \cdot \text{cost}_e
布局优化的总目标:让所有布局不一致的边产生的转换代价总和最小。
  1. 指示函数\mathbf{1}[\text{条件}] 表示条件成立时取 1,否则取 0;只为真正不一致的边计费。
    1[Lein≠Leout]\displaystyle \mathbf{1}\left[L_e^{\text{in}} \neq L_e^{\text{out}}\right]
  2. 累加所有边把每条不一致边的转换代价相加,得到全图的转换总代价。
    ∑e∈E⋯⋅coste\displaystyle \sum_{e \in E} \cdots \cdot \text{cost}_e
  3. 选最优\min_L 表示在所有可行的全图布局方案中,选出总代价最小的那一个。
    min⁡L\displaystyle \min_{L}

布局优化算法

小算终于要设计自己的调度算法了。他有两把刷子:一把叫“贪心”,眼睛只盯着当前这一步;一把叫“动态规划”(DP),心里装着全局。两把刷子配合使用,才能既快又稳。

贪心法从输出端开始,为每个算子挑一个“局部代价最小”的布局;速度极快,但可能因为只看到眼前而错过更优的全局解。DP 法则枚举每个算子的若干候选布局,用状态转移方程累计最小代价,在候选集有限时能保证找到全局最优。

真正的编译器通常把两者结合:先用启发式规则缩小候选布局集合,再用 DP 或类似搜索求解。成本模型(cost model)负责估算每个算子在某种布局下的执行时间——它本身可能是个小型回归模型或经验表,这也是布局优化能持续迭代的关键。

小算把 DP 表格贴满了一面墙,每个格子都是一条决策路径。他发现,所谓“最优布局”,不过是在每一站都问一次“从哪条路走过来最省”,然后把答案记下来。算法之美,就是把麻烦留给表格,把清爽留给流水线。

dp[i][l]=min⁡l′(dp[i−1][l′]+cost(l′→l))\displaystyle dp[i][l] = \min_{l'}\left( dp[i-1][l'] + \text{cost}(l' \rightarrow l) \right)
动态规划:第 i 个算子选布局 l 的最小代价,等于前 i−1 个算子的最优代价加上从 l' 转到 l 的花费。
  1. 状态定义dp[i][l] 表示处理到第 i 个算子、它采用布局 l 时的累计最小代价。
    dp[i][l]\displaystyle dp[i][l]
  2. 状态转移上一步布局是 l',从 l' 转到 l 需要转换代价,对所有可能的 l' 取最小值。
    min⁡l′(dp[i−1][l′]+cost(l′→l))\displaystyle \min_{l'}\left( dp[i-1][l'] + \text{cost}(l' \rightarrow l) \right)

性能影响分析

货架怎么摆,直接影响师傅的脚程。如果炒菜师傅要的四种调料在四面墙上,他炒一道菜要跑四趟;如果集成一个调料架,一趟全拿齐。速度差异肉眼可见,布局对 AI 模型的影响也是同样的道理。

布局对性能的影响主要有三处。第一是访存局部性:NHWC 让同一空间位置的通道数据连续,卷积滑动窗口时读的全是相邻地址,缓存命中率大幅提升。第二是 SIMD 向量化:通道在最内层时,一条指令可以加载多个通道的同一像素,计算密度飙升。第三是内存带宽:消除了转置的读改写,梯度回传时的 gather/scatter 也变得更规整。向量化利用率是对第二点的量化:

实测数据最有说服力:ResNet-50 在 GPU 上用 NHWC 配合 Tensor Core,比 NCHW 快 1.3~2 倍;BERT 的矩阵乘在某些加速卡上,布局错了甚至会触发全量拷贝,单算子慢 10 倍。布局优化的收益不是理论上的“一点点”,而是量级上的。

小算把师傅们的脚程数据画成热力图,发现所有慢的站点都贴着“布局不匹配”的标签。他立刻明白:调度主管的最高荣誉,不是把货架擦得多亮,而是让每位师傅少跑路。

util=used_lanestotal_lanes×100%\displaystyle \text{util} = \frac{\text{used\_lanes}}{\text{total\_lanes}} \times 100\%
向量化利用率:一次 SIMD 指令里,真正干活的通道数占比,布局越匹配越接近 100%。
  1. 分子used_lanes 是一条 SIMD 指令实际用到的车道数,比如加载 4 个 float 时用到 4 个。
    used_lanes\displaystyle \text{used\_lanes}
  2. 分母total_lanes 是硬件支持的总车道数,比如 AVX2 一条指令能处理 8 个 float。
    total_lanes\displaystyle \text{total\_lanes}
  3. 百分比乘 100% 得到利用率。布局不匹配时可能只有 1/8 的车道在干活,匹配时接近 100%。
    ×100%\displaystyle \times 100\%

工程实践调优

再完美的调度方案也需要日报来校正。小算每天早上第一件事,是看流水线的监控屏:哪站堵了,搬运工老张在哪儿忙,一目了然。没有监控,再好的理论也只是纸上谈兵。

工程中定位布局问题,靠的是 profiler。PyTorch 的 torch.profiler 能列出每个算子的耗时,如果看到大量 copy_、permute、contiguous 节点,基本可以断定布局转换在拖后腿。Nsight 等工具还能进一步看到访存指令与缓存未命中。定位之后,再用 memory_format=torch.channels_last 显式指定模型输入布局,或调整图中关键算子的输入格式。调优效果看这个指标:

常见问题清单:模型用 nn.Conv2d 默认 NCHW,但输入数据是 NHWC——第一个算子就插转换;自定义算子不支持 channels_last,导致下游全部转回 NCHW;多分支网络在汇合处布局冲突,产生隐性转换。解决思路统一:把“谁需要什么布局”的表格列出来,让布局从源头一致。

小算的日报本上,红圈越来越少。他给新人培训时总说:布局优化不是魔法,是“提前规划 + 持续观测”。把这句话抄在仓库门口,流水线就再也没堵过。

overhead=TconvertTtotal×100%\displaystyle \text{overhead} = \frac{T_{\text{convert}}}{T_{\text{total}}} \times 100\%
转换开销占比:布局转换耗时占整张图总耗时的比重,是衡量布局优化是否到位的关键指标。
  1. 分子T_convert 是 profiler 统计出的所有布局转换算子的总耗时。
    Tconvert\displaystyle T_{\text{convert}}
  2. 分母T_total 是整个模型执行一次前向/后向的端到端耗时。
    Ttotal\displaystyle T_{\text{total}}
  3. 标杆占比越高说明布局越乱;工程上通常要把这个数字压到 5% 以下才算健康。
    ×100%\displaystyle \times 100\%

AI 应用场景

场景 1:视觉模型训练加速

阶段:模型训练阶段(编译器后端优化)

解决的问题:GPU 上 nn.Conv2d 默认接收 NCHW 输入,但 cuDNN 的 NHWC 卷积路径能更好利用 Tensor Core。每次卷积前都要把 NCHW 转成 NHWC,一批 512 张图就要多搬 512×3×224×224 个元素,训练吞吐被白白浪费。

具体案例:某团队训练 EfficientNet-B0,用 torch.compile 后发现前向里有 11 个 copy_ 节点,profiler 显示转换耗时占总训练步的 15%。将模型通过 model.to(memory_format=torch.channels_last) 改为通道末尾布局后,转换节点消失,训练吞吐提升 22%。

场景 2:推理引擎的布局重写

阶段:模型部署阶段(图优化 pass)

解决的问题:推理时输入数据往往是 NHWC(如摄像头帧),而 ONNX 模型内部是 NCHW。若不在引擎层做布局传播,端侧就得手写大量转置,延迟和内存带宽双双告急。

具体案例:某工业视觉项目用 TVM 部署 YOLOv8,日志显示 ALT 自动把若干卷积的输入布局改成 NHWC,将 6 个 layout convert 融合为 1 个,端到端延迟从 18ms 降到 12ms。

场景 3:大模型推理的 KV Cache 布局

阶段:推理内存规划阶段

解决的问题:KV cache 的形状通常是 [batch, num_heads, seq_len, head_dim],而 GEMV 计算按 head 访问时更希望 head_dim 连续(类似 NHWC 的“通道在最内”)。布局一旦选错,每个 token 生成都要重排缓存,代价巨大。

具体案例:vLLM 的 PagedAttention 特意把 KV cache 按 [num_heads, head_dim] 作为最内层连续块存储,配合块分配器避免搬运,使连续批处理的吞吐提升了约 3 倍。

Python 演示

python51 行
# 布局分配模拟器:从输出端回溯,为计算图选择布局# 目标是尽量减少布局转换(搬运)的次数 class Op:    """一个算子:记录名字、期望的输入输出布局"""    def __init__(self, name, in_layout, out_layout):        self.name = name        self.in_layout = in_layout        self.out_layout = out_layout def assign_layouts(ops):    """从最后一个算子开始,向前为每条边选定布局"""    n = len(ops)    edges = [None] * (n + 1)          # 每条边的最终布局    edges[n] = ops[-1].out_layout     # 输出端布局固定    converts = 0    for i in range(n - 1, -1, -1):        op = ops[i]        if edges[i + 1] == op.out_layout:            edges[i] = op.in_layout        else:            converts += 1             # 需要插一次搬运            edges[i] = op.in_layout    return edges, converts def naive_layouts(ops):    """朴素方案:每个算子都坚持用自己最舒服的布局"""    n = len(ops)    edges = [None] * (n + 1)    converts = 0    for i, op in enumerate(ops):        if edges[i] is not None and edges[i] != op.in_layout:            converts += 1             # 上游不配合,插转换        edges[i] = op.in_layout        edges[i + 1] = op.out_layout    return edges, converts def main():    # 计算图:Conv(NCHW->NHWC) -> ReLU(NHWC->NHWC) -> Softmax(NHWC->NHWC)    ops = [        Op("Conv",    "NCHW", "NHWC"),        Op("ReLU",    "NHWC", "NHWC"),        Op("Softmax", "NHWC", "NHWC"),    ]    e1, c1 = assign_layouts(ops)    e2, c2 = naive_layouts(ops)    print("回溯传播方案:", c1, "次转换,边布局", e1)    print("朴素方案:", c2, "次转换,边布局", e2) if __name__ == "__main__":    main()
目的:用 30 行代码模拟编译器中的布局传播——从输出端回溯为每条边选定布局,并把结果与“每个算子只顾自己”的朴素方案对比,直观展示布局优化消除了不必要的转置。
  1. class Op:定义一个算子,记录它的名字和期望的输入/输出布局,就像仓库里每位师傅的取料偏好。
  2. edges = [None] * (n + 1)图中 n 个算子会形成 n+1 条数据边,这里给每条边准备一个布局位。
  3. edges[n] = ops[-1].out_layout从最后一道工序开始,输出端的布局是事先定好的(如同成品菜的装盘方式固定)。
  4. if edges[i + 1] == op.out_layout:检查下游能不能接住当前算子的输出布局,能接住就说明这一站“顺手”。
  5. converts += 1接不住就记一次“搬运”,对应图中插入一次布局转换节点。
  6. edges[i] = op.in_layout把当前算子的输入布局继续传给上游,实现从后往前的约束传播。
  7. def naive_layouts(ops):对照用的朴素方案:每个算子只坚持自己最舒服的布局,不管上下游死活。

常见误区

误区学完布局优化后,我以为它就是在图里自动加 transpose 节点,让每个算子都能拿到自己想要的布局,这已经是最优解了。

正解恰恰相反,插入转置是“下下策”。布局优化的核心是分配:提前为每个算子选好输入/输出布局,让边上根本不需要转置。只有当约束实在无法满足时才插入转换。就像仓库调度,先调整摆货方式,而不是让搬运工不停搬。

为何常见:因为编译日志里最常见到的操作就是 permute / transpose / copy,初学者误以为这就是优化动作本身,而忽略了背后更重要的布局选择决策。

误区大家都说 NHWC 利于卷积、利于向量化,那我干脆把所有模型都改成 NHWC 不就完了?

正解布局没有绝对好坏。GPU 卷积与 Tensor Core 偏向 NHWC,但 CPU 上 many 优化库对 NCHW 的支持更成熟;全连接、归一化、注意力算子也可能偏好别的布局。正确做法是让编译器按算子类型和硬件分别选择,再全局协调。

为何常见:网上教程常以 GPU 卷积为例展示 NHWC 优势,“一招鲜”的说法容易让人误以为它普适。

误区反正张量数值一样,只是排法不同,性能最多差个百分之几,不值得花大力气优化。

正解实测差距往往是倍数的。访存连续与否直接决定缓存命中、SIMD 利用率和内存带宽。比如一次 NCHW→NHWC 的转换就要读写整张特征图,在 ResNet 里可能占单层耗时的 30%。布局不是“少量性能”,是“从差到优”的关键。

为何常见:小模型或小 batch 时差距不大,容易让人低估;大模型、大批量时差距被放大数倍。

误区一个张量也就几百万元素,转置一次拷贝一遍,摊到整个训练里可以忽略。

正解O(N) 的每一步都对应一次全局内存读写,而内存带宽恰恰是 AI 计算的最大瓶颈。一次转换可能是 1 毫秒,但全图几十处转换累积起来就拖慢整个 step;更糟的是它会打断算子融合,把本该留在寄存器或缓存里的中间结果打回内存。

为何常见:人们习惯用“算法复杂度”而不是“访存成本”来评估算子,忽略了内存墙效应。

面试题

Q1. 一个形状为 (2,3,4,5) 的张量按 NCHW 行优先存储,C 维的步长是多少?

单选题 · easy

  • A.等于 20← 正确答案
  • B.等于 60
  • C.等于 5
  • D.等于 1

答案:A。NCHW 下 C 维的步长等于它右侧所有维度的尺寸乘积,即 H×W = 4×5 = 20。60 是 N 维的步长,5 是 H 维的步长,1 是 W 维的步长,都不符合题意。

Q2. 行优先(row-major)存储的核心特点是?

单选题 · easy

  • A.最内层维变化最快← 正确答案
  • B.最外层维变化最快
  • C.所有维度变化一样快
  • D.数据全部按列存放

答案:A。行优先从最内层维度开始连续存放,所以最内层维度的地址变化最快,步长为 1。这与列优先(column-major)正好相反,后者是最外层维度变化最快。

Q3. NHWC 布局下,同一空间位置 (h,w) 的多个通道数据在内存中?

单选题 · mid

  • A.完全相邻连续← 正确答案
  • B.每个通道隔一行
  • C.分散在不同内存页
  • D.与批次号绑定排列

答案:A。NHWC 把 C 放在最内层,所以同一 (h,w) 位置的不同通道在内存中彼此相邻,可以直接用一条 SIMD 指令加载。这就像把炒菜要用的四种调料放在同一个调料架上。

Q4. 下列哪些情况会在计算图中产生布局转换节点?

多选题 · mid

  • A.上游输出布局与下游需求不符← 正确答案
  • B.输入张量布局不是算子期望的← 正确答案
  • C.所有算子布局完全一致
  • D.模型使用混合精度训练

答案:AB。布局转换的唯一触发条件是“布局不一致”。A 和 B 都是典型的不一致场景。C 完全一致自然不需要转换;D 的混合精度属于数据类型问题,与布局无关,不会产生布局转换。

Q5. XLA 编译器中负责布局选择与传播的优化 pass 是?

单选题 · extreme

  • A.坐标下降优化器
  • B.布局分配专用流程← 正确答案
  • C.代数表达式化简器
  • D.内存池复用管理器

答案:B。XLA 在 HLO 层面通过 LayoutAssignment pass 为每个指令指定布局并传播约束,对应选项 B。A 是数值优化方法,C 是代数化简,D 是内存复用,都与布局选择无关。

Q6. PyTorch 中让模型内部以 channels_last(NHWC)布局运行的常用写法是?

单选题 · mid

  • A.调用模型连续化接口
  • B.把模型整体搬到 GPU
  • C.将模型参数量减半
  • D.指定内存格式为通道末尾← 正确答案

答案:D。正确做法是 model.to(memory_format=torch.channels_last),即 D。A 的 contiguous() 只是强制当前张量连续化,不改变维度顺序;B 只影响设备;C 与布局无关。

Q7. 布局优化能为模型带来哪些实际收益?

多选题 · mid

  • A.减少数据搬运开销← 正确答案
  • B.提升访存的局部性← 正确答案
  • C.提高缓存命中概率← 正确答案
  • D.减小模型文件体积

答案:ABC。布局优化通过消除转置减少搬运、通过连续访存提升局部性和缓存命中率,A、B、C 都对。它改变的是内存排列方式,不改变参数数量和精度,因此不会减小模型文件体积,D 错误。

Q8. 一段 NCHW 数据真正转成紧凑的 NHWC,本质上经历了什么?

单选题 · hard

  • A.仅改步长不搬内存
  • B.换维序后连续化拷贝← 正确答案
  • C.逐元素做精度转换
  • D.在显存中多存备份

答案:B。NCHW→NHWC 是维度重排(permute),但原布局下非最内层维度不连续,必须再做一次 contiguous 拷贝才能得到紧凑的 NHWC。A 只改元数据无法重排数据;C 是精度问题;D 是冗余存储策略。

Q9. 布局传播阶段,编译器通常从哪个方向进行约束回溯?

单选题 · mid

  • A.从图输入端开始
  • B.从图输出端开始← 正确答案
  • C.从图中间随机开始
  • D.从所有节点同时蔓延

答案:B。输出端的布局通常是确定的(如 Softmax 要求 NHWC),从输出端往回推,每一步都能依据已知约束确定当前算子的输入布局,从而最小化转换插入。这与仓库里“从成品菜倒推备菜摆放”是同一个思路。

Q10. 关于布局转换代价,下列说法正确的有?

多选题 · hard

  • A.代价随元素数增长← 正确答案
  • B.转换会打断算子融合← 正确答案
  • C.转换只在 GPU 上发生
  • D.融合后更需要消除转换← 正确答案

答案:ABD。布局转换需要读写全部元素,代价 O(元素数),A 正确;转换会把中间结果打回内存,切断融合链,B 正确;正因为转换破坏融合,算子融合后更要消除转换,D 正确。C 错误,CPU 上同样存在布局转换问题。