← CuTe 笔记 / examples/77_blackwell_fmha/MAKE_COTILED_COPY_Q_LAYOUT_zh.md · 下载原文件
1# 从源码理解 Q 的 `make_cotiled_copy`:为什么 `replace` 使用 `head_dim`
2
3本文对应 CUTLASS 提交 `0b55a2f6`,沿 `77_blackwell_fmha_gen.cu` 的 decode/generation 路径展开。主要使用当前示例的 FP8 输入、`D=128`;最后对照 FP16。
4
5这里讨论的是 `Sm100FmhaLoadCpAsyncWarpspecialized::load()` 中的调用,不能直接套用 prefill 的 `run_fwd_128()`、`TileShape=(256,128,128)` 或其 `ThreadShape`。
6
7## 1. 核心结论
8
9调用点:
10
11```cpp
12auto tiled_copy_q = make_cotiled_copy(
13 Copy_Atom<SM80_CP_ASYNC_CACHEALWAYS<uint128_t>, Element>{},
14 atom_q_tv,
15 make_layout(
16 shape(tSgQ),
17 replace<0>(
18 stride(tSgQ),
19 replace<0>(
20 stride<0>(tSgQ),
21 get<2>(TileShape{})))));
22```
23
24来源:[sm100_fmha_load_cpasync_warpspecialized.hpp:176](/home/x/workspace/cutlass/examples/77_blackwell_fmha/collective/sm100_fmha_load_cpasync_warpspecialized.hpp:176)。
25
26两层 `replace<0>` 实际替换的是 **fragment 内 M 方向的 stride**,不是 MMA 的 `ThrV` stride。
27
28在当前紧凑存储的示例中,这个 stride 的运行时数值已经等于 `head_dim`。替换将它从普通 `int` 变成类型中编码的 `cute::Int<D>`:
29
30```text
31运行时 int,值为 128
32 ↓
33编译期 cute::Int<128>
34```
35
36这样构造出的 `data_layout` 才满足 `make_cotiled_copy` 对静态布局的要求。
37
38选择 `head_dim` 的依据是复制参考布局的行组织:
39
40```text
41一行 = 一个 Q head 的 D 个连续特征
42参考地址偏移 = m * D + d
43```
44
45原来的 `tSgQ` 没有被修改。静态参考布局用于生成 copy 的逻辑坐标分工,真实源 tensor 仍使用其原有 stride 计算访存地址。
46
47## 2. 本文的参数和记号
48
49选择当前示例中的 `TileShape=(128,128,128)`。
50
51| 记号 | 含义 | 本文取值 |
52|---|---|---:|
53| `D` | Q/K 的 feature dimension,即 `head_dim` | 128 |
54| `G` | 同一个 KV head 对应的 Q heads 数量,即 `Hq/Hkv` | 运行时参数 |
55| `Hkv` | KV heads 数量 | 运行时参数 |
56| `B` | batch 数量 | 运行时参数 |
57| `M_tile` | Q tile 的 M 大小 | 128 |
58| `K_atom` | MMA atom 的 K 大小,FP8 下为 32 | 32 |
59| `t` | 展平的 copy thread id | 0–63 |
60| `v` | 一次 copy 分工内的 value id | 0–15 |
61
62CuTe 的布局打印采用 `shape:stride` 格式:
63
64```text
65_128:编译期常量 cute::Int<128>
66 128:运行时整数,其当前值为 128
67```
68
69后文区分三个整数空间:
70
71| 记号 | 含义 |
72|---|---|
73| `a` | 参考布局的地址偏移,单位是元素 |
74| `q` | data tensor 坐标的默认线性编号 |
75| `z` | copy tile 坐标的默认线性编号 |
76
77地址偏移和坐标编号不能混为一谈。
78
79## 3. decode 为什么把 Q heads 放到 M 维
80
81### 3.1 原始输入
82
83[77_blackwell_fmha_gen.cu:329](/home/x/workspace/cutlass/examples/77_blackwell_fmha/77_blackwell_fmha_gen.cu:329) 定义:
84
85```cpp
86using Element = cutlass::float_e5m2_t;
87
88using ProblemShape =
89 Shape<_1, int, int, Shape<Shape<int, int>, int>>;
90
91using StrideQ =
92 Stride<_0, _1, Stride<Stride<int, int>, int>>;
93```
94
95问题形状是:
96
97```text
98(1, N, D, ((G,Hkv),B))
99```
100
101decode 只有一个 query token,原始 Q 的视图为:
102
103```text
104Q: (1, D, ((G,Hkv),B))
105```
106
107[初始化代码](/home/x/workspace/cutlass/examples/77_blackwell_fmha/77_blackwell_fmha_gen.cu:472) 将 Q 的 stride 设为:
108
109```text
110(0, 1, ((D, D*G), D*G*Hkv))
111```
112
113其中 `D`、`G`、`Hkv` 来自运行时 options。相邻同组 Q heads 之间的跨度为 `D`。
114
115### 3.2 kernel 转换参数
116
117[sm100_fmha_gen_kernel_warpspecialized.hpp:205](/home/x/workspace/cutlass/examples/77_blackwell_fmha/kernel/sm100_fmha_gen_kernel_warpspecialized.hpp:205):
118
119```cpp
120StrideQ dQ = replace<0>(args.dQ, 0);
121
122get<0>(problem_shape) = get<3,0,0>(args.problem_shape);
123get<3,0,0>(problem_shape) = 1;
124
125get<0>(dQ) = get<2,0,0>(dQ);
126```
127
128它将同组 Q heads 的维度 `G` 移到 M 维:
129
130```text
131原问题: (1, N, D, ((G,Hkv),B))
132转换后: (G, N, D, ((1,Hkv),B))
133```
134
135同时将原来的 Q head stride 搬到 M stride:
136
137```text
138dQ.M_stride = 原来的 G_stride = D
139```
140
141因此,进入加载器后,M 表示同组的不同 Q heads,而不是多个 query tokens。
142
143**不能只看 `replace<0>(args.dQ,0)` 就认为最终 M stride 是 0。后面的赋值将它设成了 head stride。**
144
145## 4. 为什么源布局的 M stride 是动态的
146
147[mainloop 的类型定义](/home/x/workspace/cutlass/examples/77_blackwell_fmha/collective/sm100_fmha_gen_mainloop_warpspecialized.hpp:75):
148
149```cpp
150using StrideQOrig = StrideQ_;
151using StrideQ = decltype(replace<0>(StrideQ_{}, 0));
152```
153
154这里使用的是普通整数 `0`,不是 `_0{}`:
155
156```cpp
1570 // 类型为 int,值存储在对象中
158_0{} // 类型为 cute::Int<0>,值编码在类型中
159```
160
161这句将第一个 stride 的类型改为 `int`。kernel 随后才能把调用方的 head stride 赋值给它。
162
163当前示例的赋值结果是:
164
165```text
166M stride 的类型 = int
167M stride 的数值 = 128
168```
169
170赋值只改变数值,不会将 `int` 的类型变成 `cute::Int<128>`。
171
172CuTe 对静态值的判定关注的是值是否由类型完全定义。相关定义见 [integral_constant.hpp:90](/home/x/workspace/cutlass/include/cute/numeric/integral_constant.hpp:90):
173
174```cpp
175// is_static detects if an (abstract) value is defined completely by its type
176template <class T>
177struct is_static : bool_constant<is_empty<T>::value> {};
178```
179
180因此,即使某个普通 `int` 的当前值或实际使用值始终是 128,它仍然不是 CuTe 的静态常量。
181
182## 5. 从 `gQ` 到 `tSgQ`
183
184### 5.1 mainloop 的实际 tile
185
186[sm100_fmha_gen_mainloop_warpspecialized.hpp:65](/home/x/workspace/cutlass/examples/77_blackwell_fmha/collective/sm100_fmha_gen_mainloop_warpspecialized.hpp:65):
187
188```cpp
189class ThreadShape = Shape<_1, _2, _1>;
190
191using TileShapeQK =
192 decltype(shape_div(TileShape{}, ThreadShape{}));
193```
194
195本例:
196
197```text
198TileShape = (128,128,128)
199ThreadShape = ( 1, 2, 1)
200TileShapeQK = (128, 64,128)
201```
202
203本版本 `ExampleRunner` 虽然声明了 `ThreadShape` 模板参数,但其构造 mainloop 的代码没有转发该参数,所以这里按 mainloop 的默认 `(1,2,1)` 展开,不能直接把 `RUN` 中打印的 thread shape 当成 mainloop 实际配置。见 [77_blackwell_fmha_gen.cu:326](/home/x/workspace/cutlass/examples/77_blackwell_fmha/77_blackwell_fmha_gen.cu:326)。
204
205### 5.2 构造 Q tensor
206
207[加载器源码](/home/x/workspace/cutlass/examples/77_blackwell_fmha/collective/sm100_fmha_load_cpasync_warpspecialized.hpp:163):
208
209```cpp
210auto cQ = make_identity_tensor(select<0,2>(TileShape{}));
211
212auto mQ = make_tensor(
213 make_gmem_ptr(params.ptr_q),
214 append<3>(
215 select<0,2>(TileShapeQK{}),
216 get<3>(problem_shape)),
217 params.dQ);
218
219auto gQ = mQ(_, _, get<2>(blk_coord));
220auto sQ = make_tensor(
221 make_smem_ptr(storage.smem_q.data()), SmemLayoutQ{});
222```
223
224固定 head group 和 batch 后:
225
226```text
227gQ.shape = (_128,_128)
228gQ.stride = (128,_1)
229```
230
231M tile 为 128,但有效 M 行数为运行时的 `G`,超出的行由后续掩码处理。
232
233### 5.3 MMA atom
234
235builder 为 FP8 选择 `SM100_MMA_F8F6F4_SS`,见 [sm100_common.inl:331](/home/x/workspace/cutlass/include/cutlass/gemm/collective/builders/sm100_common.inl:331)。
236
237其 [trait](/home/x/workspace/cutlass/include/cute/atom/mma_traits_sm100.hpp:2485):
238
239```cpp
240constexpr static int K = 32;
241
242using Shape_MNK = Shape<Int<M>,Int<N>,Int<K>>;
243using ThrID = Layout<_1>;
244
245using ALayout =
246 Layout<Shape<_1,Shape<Int<M>,Int<K>>>,
247 Stride<_0,Stride<_1,Int<M>>>>;
248```
249
250本例:
251
252```text
253MMA atom = (128,64,32)
254RestM = 128 / 128 = 1
255RestK = 128 / 32 = 4
256```
257
258`ALayout` 内的 stride 是 MMA 的逻辑 `(M,K)` 坐标映射;组合到源 tensor 后,实际访存 stride 来自 `gQ`。
259
260### 5.4 `thrfrg_A` 和 `partition_A` 必须分开看
261
262[`thrfrg_A()`](/home/x/workspace/cutlass/include/cute/atom/mma_atom.hpp:291) 依次执行:
263
264```text
265按 permutation 划分
266 → 按 MMA atom 划分
267 → compose AtomLayoutA_TV
268 → 按 MMA thread tiling 划分
269```
270
271其输出结构为:
272
273```text
274((ThrV,(ThrM,ThrK)), (FrgV,(RestM,RestK)))
275```
276
277本例实际布局:
278
279```text
280((_1,(_1,_1)),((_128,_32),(_1,_4))):
281((_0,(_0,_0)),((128,_1),(_0,_32)))
282```
283
284但是 [`partition_A()`](/home/x/workspace/cutlass/include/cute/atom/mma_atom.hpp:478) 继续固定线程坐标:
285
286```cpp
287auto thr_tensor = make_tensor(
288 static_cast<ATensor&&>(atensor).data(),
289 this->thrfrg_A(atensor.layout()));
290
291auto thr_vmk = make_coord(
292 get<0>(thr_vmnk_),
293 make_coord(get<1>(thr_vmnk_), get<3>(thr_vmnk_)));
294
295return thr_tensor(
296 thr_vmk,
297 make_coord(_, repeat<rank<1,1>(thr_tensor)>(_)));
298```
299
300线程 mode 被切掉,`tSgQ` 的三个顶层 mode 是:
301
302```text
303(FrgV, RestM, RestK)
304```
305
306最终:
307
308```text
309tSgQ.shape = ((_128,_32),_1,_4)
310tSgQ.stride = ((128,_1),_0,_32)
311```
312
313令坐标为 `((m,k),0,r)`:
314
315```text
316d = k + 32*r
317源地址偏移 = 128*m + k + 32*r = D*m + d
318```
319
320这个布局的 shape 全部静态,但 stride 中仍含有一个 `int`,因此整个 layout 不是完全静态的。
321
322## 6. 两层 `replace<0>` 分别替换哪里
323
324首先:
325
326```text
327stride<0>(tSgQ) = (128,_1)
328```
329
330这是 `FrgV=(M_atom,K_atom)` 内的两个 stride。
331
332内层替换:
333
334```cpp
335replace<0>(stride<0>(tSgQ), get<2>(TileShape{}))
336```
337
338结果:
339
340```text
341(128,_1) → (_128,_1)
342```
343
344外层替换:
345
346```cpp
347replace<0>(stride(tSgQ), /* 修改后的 FrgV stride */)
348```
349
350结果:
351
352```text
353((128,_1),_0,_32)
354 ↓
355((_128,_1),_0,_32)
356```
357
358所以 `data_layout` 为:
359
360```text
361((_128,_32),_1,_4):((_128,_1),_0,_32)
362```
363
364本例中它与原源布局的数值映射一致,但它是静态布局。
365
366[`make_cotiled_copy()`](/home/x/workspace/cutlass/include/cute/atom/copy_atom.hpp:528) 明确要求:
367
368```cpp
369static_assert(is_static<AtomTVLayout>::value);
370static_assert(is_static<DataLayout>::value);
371```
372
373[`left_inverse()`](/home/x/workspace/cutlass/include/cute/layout.hpp:1364) 也要求静态 stride,用于在编译期合并 mode、排序 stride 和构造逆映射。
374
375## 7. `make_cotiled_copy` 的完整变换
376
377### 7.1 copy TV layout 产生地址偏移
378
379```cpp
380auto atom_q_tv =
381 Layout<Shape<Shape<_2,_32>,_16>,
382 Stride<Stride<_16,_32>,_1>>{};
383```
384
385这表示 64 个线程,每个线程 16 个元素。将线程编号展平为 `t`:
386
387```text
388a(t,v) = 16*(t % 2) + 32*floor(t/2) + v
389 = 16*t + v
390```
391
392一次参考分工覆盖地址偏移 `0..1023`。
393
394这里的 `atom_q_tv` 是提供给构造器的线程和值偏移图;硬件 copy 指令的粒度由第一个参数 `Copy_Atom<...,Element>` 决定,二者不应混淆。
395
396### 7.2 `left_inverse(data_layout)` 返回 data 坐标编号
397
398源码:
399
400```cpp
401auto inv_data_layout =
402 make_layout(left_inverse(data_layout), Layout<_1,_0>{});
403
404auto layout_tv_data = composition(inv_data_layout, atom_tv_layout);
405```
406
407对坐标 `((m,k),0,r)`,参考地址偏移为:
408
409```text
410a = 128*m + k + 32*r
411 = 128*m + d
412```
413
414同一坐标的默认线性编号为:
415
416```text
417q = m + 128*k + 4096*r
418 = m + 128*d
419```
420
421逆映射从 `a` 得到:
422
423```text
424m = floor(a/128)
425d = a % 128
426q = m + 128*d
427```
428
429实际结果:
430
431```text
432coalesce(data_layout) = (_128,_128):(_128,_1)
433left_inverse = (_128,_128):(_128,_1)
434inv_data_layout = ((_128,_128),_1):((_128,_1),_0)
435```
436
437由于本例的 M 和 D 都是 128,正向布局和逆布局的打印形式碰巧一样;两者的输入输出空间不同。
438
439追加的 `Layout<_1,_0>` 用于让超出末端的输入落到 stride-0 mode。本例 copy 地址都在正常范围内。
440
441### 7.3 组合后得到 `layout_tv_data`
442
443由 `a=16*t+v` 可直接得到:
444
445```text
446m = floor(t/8)
447d = 16*(t % 8) + v
448```
449
450为对照布局打印,将线程编号进一步拆成:
451
452```text
453t = t0 + 2*t1 + 8*t2
454
455t0 ∈ [0,1]
456t1 ∈ [0,3]
457t2 ∈ [0,7]
458```
459
460于是:
461
462```text
463m = t2
464k = 16*t0 + v
465r = t1
466
467q = t2 + 2048*t0 + 128*v + 4096*t1
468```
469
470实际布局:
471
472```text
473layout_tv_data =
474((_2,(_4,_8)),_16):((_2048,(_4096,_1)),_128)
475```
476
477这里的 `2048`、`4096`、`128` 是 data 坐标编号空间中的系数,不是新的 global memory stride。
478
479### 7.4 逐 mode 投影并生成 `tiler`
480
481源码:
482
483```cpp
484auto flat_data_shape = product_each(shape(data_layout));
485auto flat_data_zeros = repeat<rank(flat_data_shape)>(Int<0>{});
486
487auto tiler = transform(
488 make_seq<rank(flat_data_shape)>{},
489 [&](auto i) {
490 return filter(composition(
491 make_layout(
492 flat_data_shape,
493 replace<i>(flat_data_zeros, Int<1>{})),
494 layout_tv_data));
495 });
496```
497
498本例:
499
500```text
501flat_data_shape = (_4096,_1,_4)
502```
503
504`product_each` 将第一个顶层 mode `(128,32)` 合成 4096,但保留三个顶层 mode。
505
506投影布局的 stride 分别为 `(1,0,0)`、`(0,1,0)`、`(0,0,1)`,用于提取各 mode 的坐标。
507
508| mode | 提取的坐标 | `filter` 后的布局 |
509|---|---|---|
510| FrgV | `2048*t0 + t2 + 128*v` | `(2,8,16):(2048,1,128)` |
511| RestM | `0` | `1:0` |
512| RestK | `t1` | `4:1` |
513
514完整结果:
515
516```text
517tiler =
518(
519 (_2,_8,_16):(_2048,_1,_128),
520 _1:_0,
521 _4:_1
522)
523```
524
525它选出的 Q 坐标为:
526
527```text
528m = 0..7
529d = 0..127
530```
531
532即一次分工覆盖 `8×128=1024` 个元素。整个 Q tile 仍有 `128×128` 个元素。
533
534### 7.5 构造 `tile2data`
535
536```cpp
537auto tile2data = composition(make_layout(flat_data_shape), tiler);
538```
539
540实际结果:
541
542```text
543tile2data =
544((_2,_8,_16),_1,_4):((_2048,_1,_128),_0,_4096)
545```
546
547设 copy tile 坐标为 `((t0,m,v),0,r)`:
548
549```text
550q = 2048*t0 + m + 128*v + 4096*r
551```
552
553这里组合的是 `make_layout(flat_data_shape)`,不是物理 `data_layout`:`tile2data` 生成 data 坐标编号,而不是物理地址。
554
555### 7.6 构造最终 `layout_tv`
556
557```cpp
558auto layout_tv = composition(left_inverse(tile2data), layout_tv_data);
559```
560
561copy tile 自己的默认线性编号为:
562
563```text
564z = t0 + 2*m + 16*v + 256*r
565```
566
567代入 `m=t2`、`r=t1`:
568
569```text
570z(t,v) = t0 + 2*t2 + 16*v + 256*t1
571```
572
573因此:
574
575```text
576layout_tv =
577((_2,(_4,_8)),_16):((_1,(_256,_2)),_16)
578```
579
580最终 `make_tiled_copy_impl(copy_atom, layout_tv, tiler)` 用这些静态映射构造 `TiledCopy`。
581
582完整转换链:
583
584```text
585(tid,vid)
586 │ atom_q_tv
587 ↓
588参考地址偏移 a
589 │ left_inverse(data_layout)
590 ↓
591data 坐标编号 q
592 │ left_inverse(tile2data)
593 ↓
594copy tile 坐标编号 z
595```
596
597## 8. 如何作用到真实源、共享内存和掩码
598
599加载器接下来调用:
600
601```cpp
602auto thr_copy_q = tiled_copy_q.get_slice(thread_idx);
603
604auto tQsQ = thr_copy_q.partition_D(sQ);
605auto tQgQ = thr_copy_q.partition_S(tSgQ);
606auto tQcQ = thr_copy_q.partition_S(tScQ);
607```
608
609[`partition_S/D()`](/home/x/workspace/cutlass/include/cute/atom/copy_atom.hpp:365) 将同一份 tiler 和 TV 映射分别组合到各个 tensor 的布局上。
610
611| tensor | 用途 |
612|---|---|
613| `tQgQ` | 根据真实源布局计算 global memory 源地址 |
614| `tQsQ` | 根据共享内存布局及 swizzle 计算目的地址 |
615| `tQcQ` | 给出同一份分工对应的原始 `(m,d)` 坐标,供掩码使用 |
616
617### 8.1 `tile2thrfrg` 中的 `zip + coalesce` 详细展开
618
619`partition_S/D` 内部会先用 `zipped_divide` 按 tiler 划分真实 tensor,再由 [`tile2thrfrg()`](/home/x/workspace/cutlass/include/cute/atom/copy_atom.hpp:257) 将 tile 内的坐标转换成线程和值。
620
621核心代码:
622
623```cpp
624auto atom_layout_TV = zipped_divide(
625 TiledLayout_TV{}, make_shape(AtomNumThr{}, AtomNumVal{}));
626
627auto trg_layout_TV = atom_layout_TV.compose(ref2trg, _);
628
629auto thrval2mn = coalesce(
630 zip(trg_layout_TV), Shape<_1,Shape<_1,_1>>{});
631
632auto tv_tensor = tensor.compose(thrval2mn, _);
633return tv_tensor(make_coord(_,_), _);
634```
635
636注意准确写法是 `Shape<_1,Shape<_1,_1>>{}`,不是 `Shape<1,...>`。`Shape` 接受类型参数,`_1` 是 CuTe 的常量类型。
637
638#### 8.1.1 进入这一句之前的层级
639
640`TiledLayout_TV` 描述整个 copy tile 的 `(thread,value)` 分工。`zipped_divide` 按硬件 Copy Atom 的线程数和值数将它分为:
641
642```text
643((atom_tid,atom_val),(rest_tid,rest_val))
644```
645
646其中 `rest_tid`、`rest_val` 是当前 copy tile 内相对 Atom 的重复部分,不是 Q tensor 的 `RestM`、`RestK`,也不是 tile 外的剩余坐标。
647
648`ref2trg` 的源码变量名需要结合实际表达式理解。源端传入:
649
650```cpp
651right_inverse(AtomLayoutRef{}).compose(AtomLayoutSrc{})
652```
653
654它接收 source Atom 的线程和值坐标,先找到该值的 reference 编号,再送入 `atom_layout_TV` 的第一个 mode。目的端将 `AtomLayoutSrc` 换成 `AtomLayoutDst`。因此转换后的输入结构是:
655
656```text
657((trg_tid,trg_val),(rest_tid,rest_val))
658```
659
660本例 cp.async 的 Src、Dst、Ref 布局相同,转换不改变元素关系;其他 Copy Atom 不应直接套用这一简化。
661
662这里的 `TiledLayout_TV` 始终按 `AtomLayoutRef` 的坐标解释,同一个 `TiledCopy` 在 `partition_S/D` 时不会更换它。源端通过 `Ref^-1∘Src` 转成 reference 坐标,目的端通过 `Ref^-1∘Dst` 转成 reference 坐标,再应用统一的 reference TV 分工。
663
664`make_cotiled_copy` 的 `data_layout` 只是 tensor 的参考坐标到地址映射;选用源 tensor 或目的 tensor 的布局构造它,不会改变 `Copy_Traits::RefLayout`。若手头已有某一侧期望的 TV 分工,需要结合 Atom 的 Ref 定义构造 reference TV,再检查最终 `get_layoutS_TV()` 和 `get_layoutD_TV()`,而不是把期望的源分工直接冒充目的参考分工。
665
666Ref=Dst 的 ldmatrix 具体例子和两端转换公式,见 [make_tiled_copy 教程的参考端说明](/home/x/workspace/cutlass/cute_make_tiled_copy_walkthrough_zh.md)。
667
668#### 8.1.2 `zip` 改变分组,未搬动数据
669
670[`zip(Layout)`](/home/x/workspace/cutlass/include/cute/layout.hpp:1557) 同时转置 shape 和 stride 的 tuple:
671
672```cpp
673return make_layout(zip(layout.shape()), zip(layout.stride()));
674```
675
676效果为:
677
678```text
679((trg_tid,trg_val),(rest_tid,rest_val))
680 ↓ zip
681((trg_tid,rest_tid),(trg_val,rest_val))
682 线程 值
683```
684
685对应坐标仍指向同一个 tile 元素:
686
687```text
688trg_layout_TV((ta,va),(tr,vr))
689 == zip(trg_layout_TV)((ta,tr),(va,vr))
690```
691
692这不是 tensor 数据转置,也不是跨线程通信。由于输入坐标结构发生变化,不能不加坐标变换就声称 zip 前后对同一个标量输入 i 的输出总相同。
693
694#### 8.1.3 profile 保留哪些边界
695
696`Shape<_1,Shape<_1,_1>>{}` 是递归控制结构:
697
698```text
699(
700 终端, // 将整个线程子布局交给普通 coalesce
701 (终端,终端) // 分别简化 Atom 内的值、Atom 重复的值
702)
703```
704
705[`coalesce(layout,profile)`](/home/x/workspace/cutlass/include/cute/layout.hpp:880) 遇到 tuple profile 时继续逐 mode 递归,遇到标量 profile 时调用普通 `coalesce(layout)`。标量终端的数值不参与计算,所以 `(7,(7,7))` 在这里与 `(1,(1,1))` 等效。
706
707这个 profile 不是目标 shape,也不要求每个 mode 的大小变成 1。可以将其理解为:
708
709```cpp
710auto z = zip(trg_layout_TV);
711auto thrval2mn = make_layout(
712 coalesce(get<0>(z)),
713 make_layout(
714 coalesce(get<1,0>(z)),
715 coalesce(get<1,1>(z))));
716```
717
718普通 coalesce 会展平指定子布局、去掉静态 size-1 mode,并在相邻 mode 满足连续条件时合并。例如 `(2,4):(1,2)` 可以变为 `8:1`,但 `(2,4):(1,256)` 不能这样合并。
719
720因此线程子布局可能仍有多个 mode;它只是简化表示,不是强制将 64 个线程写成单一的 `64`。
721
722#### 8.1.4 当前 FP8 的准确输出
723
724FP8 的 16 字节 cp.async 对应:
725
726```text
727AtomNumThr = 1
728AtomNumVal = 16
729```
730
731按第 7 节的 TiledLayout_TV 展开:
732
733```text
734TiledLayout_TV =
735((_2,(_4,_8)),_16):((_1,(_256,_2)),_16)
736
737atom_layout_TV = trg_layout_TV =
738((_1,_16),((_2,_4,_8),_1)):
739((_0,_16),((_1,_256,_2),_0))
740
741zip(trg_layout_TV) =
742((_1,(_2,_4,_8)),(_16,_1)):
743((_0,(_1,_256,_2)),(_16,_0))
744
745thrval2mn =
746((_2,_4,_8),(_16,_1)):
747((_1,_256,_2),(_16,_0))
748```
749
750线程部分去掉 size-1 的 `trg_tid`,但 `(2,4,8):(1,256,2)` 不能按连续条件合并为单个 mode。值部分保留 `(16,1)`,表示“一条 Atom 指令内 16 个值,当前 copy tile 内每线程重复 1 次 Atom”。
751
752将线程解码为 `t=t0+2*t1+8*t2`,此时:
753
754```text
755thrval2mn(t,(va,0)) = t0 + 256*t1 + 2*t2 + 16*va
756```
757
758它的输出就是第 7 节中 copy tile 的线性坐标编号 z,还不是 global memory 地址,也不是 C++ 二元 tuple `(m,n)`。
759
760#### 8.1.5 FP16 为什么要保留 `(8,2)`
761
762FP16 的 16 字节 Atom 每线程处理 8 个值,整个 copy tile 的 TV 分工仍为每线程 16 个值,因此:
763
764```text
765AtomNumThr = 1
766AtomNumVal = 8
767rest_val extent = 16/8 = 2
768```
769
770实际结果:
771
772```text
773trg_layout_TV =
774((_1,_8),((_8,_8),_2)):((_0,_8),((_128,_1),_64))
775
776zip(trg_layout_TV) =
777((_1,(_8,_8)),(_8,_2)):((_0,(_128,_1)),(_8,_64))
778
779thrval2mn =
780((_8,_8),(_8,_2)):((_128,_1),(_8,_64))
781```
782
783虽然值子布局 `(8,2):(8,64)` 满足 `8*8=64`,可以在数值上合并成 `16:8`,本句的 profile 刻意保留了这两个分组:
784
785```text
7868:一条指令内的元素
7872:重复两次 Atom 覆盖的元素
788```
789
790如果改成更浅的 `Shape<_1,_1>{}`,FP16 会得到:
791
792```text
793((_8,_8),_16):((_128,_1),_8)
794```
795
796这仍能表达元素关系,但丢掉了此处后续代码需要的值分组接口。
797
798不传 profile 则可能跨线程和值边界继续合并。FP8 本例会得到:
799
800```text
801(_2,_4,_128):(_1,_256,_2)
802```
803
804原来的线程 mode 与值 mode 已不再独立,所以不能直接替换源码中的布局。
805
806#### 8.1.6 与真实 tensor 组合
807
808`tensor` 是 `zipped_divide` 之后的真实 tensor 布局:
809
810```text
811((tile内坐标),(tile外剩余坐标)) → 真实 tensor 偏移
812```
813
814因此:
815
816```cpp
817auto tv_tensor = tensor.compose(thrval2mn, _);
818```
819
820将第一个 tile mode 的输入改成线程和值,同时保留 tile 外的剩余部分:
821
822```text
823((线程,(Atom内值,Atom重复值)), tile外剩余坐标)
824 → 真实 tensor 偏移
825```
826
827`tv_tensor(make_coord(_,_),_)` 再将第一个组合 mode 中的线程和值展开成两个顶层 mode。`ThrCopy::partition_S/D` 随后固定当前线程,形成实际的 per-thread fragment。
828
829这种写法统一了源布局、共享内存布局和坐标 tensor 的线程分工;三者可以有不同的物理布局,但使用同一组逻辑 tile 坐标。
830
831另一个 `make_tiled_copy` 例子中 `right_inverse(layout_mn)=(8,16):(16,1)` 的逐项源码推导,见 [make_tiled_copy 教程](/home/x/workspace/cutlass/cute_make_tiled_copy_walkthrough_zh.md)。不要将那个例子的 right inverse 与本例 `make_cotiled_copy` 使用的 data-layout left inverse 混淆。
832
833### 8.2 单线程如何覆盖完整 tile
834
835FP8 下,单线程源 partition 的实际布局为:
836
837```text
838shape = ((_16,_1),_16,_1,_1)
839stride = ((_1,_0),1024,_0,_0)
840```
841
842这是 16 个连续元素,沿 M 重复 16 次。令重复编号 `p∈[0,15]`:
843
844```text
845m = floor(t/8) + 8*p
846d = 16*(t % 8) + v
847```
848
849源地址相对于当前 head group 起点的偏移是:
850
851```text
852128*m + d = 16*t + v + 1024*p
853```
854
855| thread | 负责的 M 行 | 每行负责的 D 范围 |
856|---|---|---|
857| 0 | 0、8、16、…、120 | 0–15 |
858| 7 | 0、8、16、…、120 | 112–127 |
859| 8 | 1、9、17、…、121 | 0–15 |
860| 63 | 7、15、23、…、127 | 112–127 |
861
862所以:
863
864```text
86564 threads × 16 values × 16 repeats
866= 16384
867= 128 × 128
868```
869
870目的布局的物理地址可以不同,逻辑 `(m,d)` 分工保持对应。不能因为参考布局是紧凑 row-major,就认为共享内存也一定是无 swizzle 的 row-major。
871
872### 8.3 有效行数和 `cp_async_zfill`
873
874[加载代码](/home/x/workspace/cutlass/examples/77_blackwell_fmha/collective/sm100_fmha_load_cpasync_warpspecialized.hpp:187):
875
876```cpp
877auto limitQ = append<2>(get<0>(problem_shape), _128{});
878
879auto src = recast<uint128_t>(tQgQ(_, _, _, _));
880auto dst = recast<uint128_t>(tQsQ(_, _, _, _, state.index()));
881auto c = tQcQ(_, _, _, _);
882
883int vlen = sizeof(uint128_t) / sizeof(Element);
884
885for (int i = 0; i < size(src); i++) {
886 auto cc = c(vlen*i);
887 bool guard = elem_less(cc, limitQ);
888 cutlass::arch::cp_async_zfill<16, cutlass::arch::CacheOperation::Always>(
889 &dst(i), &src(i), guard);
890}
891```
892
893转换后的 `get<0>(problem_shape)=G`,所以判定相当于:
894
895```text
896(m < G) && (d < 128)
897```
898
899有效 Q heads 从 global memory 加载;超出 G 的填充行清零。不是固定只加载第 0 行,也不是将同一个 Q head 广播给全部 M 行。
900
901当前示例只支持 `D=128`,这里的 feature limit 也直接使用 `_128{}`。不能仅凭 `replace` 引用 `TileShape` 就推断整个示例已支持任意 head dimension。
902
903## 9. FP16 对照
904
905FP16 的 MMA trait 将 atom K 定为 `256/16=16`,见 [mma_traits_sm100.hpp:236](/home/x/workspace/cutlass/include/cute/atom/mma_traits_sm100.hpp:236)。
906
907同样取 Q tile `128×128`:
908
909| 布局 | FP8 | FP16 |
910|---|---|---|
911| MMA atom K | 32 | 16 |
912| `tSgQ.shape` | `((128,32),1,4)` | `((128,16),1,8)` |
913| 源 stride | `((int(128),1),0,32)` | `((int(128),1),0,16)` |
914| 静态参考 stride | `((Int<128>,1),0,32)` | `((Int<128>,1),0,16)` |
915| `flat_data_shape` | `(4096,1,4)` | `(2048,1,8)` |
916
917FP16 的实际中间布局为:
918
919```text
920data_layout =
921((_128,_16),_1,_8):((_128,_1),_0,_16)
922
923inv_data_layout =
924((_128,_128),_1):((_128,_1),_0)
925
926layout_tv_data =
927((_2,(_4,_8)),_16):((_2048,(_4096,_1)),_128)
928
929tiler =
930((_8,_16):(_1,_128),_1:_0,_8:_1)
931
932tile2data =
933((_8,_16),_1,_8):((_1,_128),_0,_2048)
934
935layout_tv =
936((_2,(_4,_8)),_16):((_128,(_256,_1)),_8)
937```
938
939FP16 和 FP8 的 fragment 层级不同,但最后的 `(m,d)` 分工一致。
940
941`atom_q_tv` 仍为每线程 16 个元素,不过每条 16 字节的 cp.async 对 FP16 只复制 8 个元素,因此每个这样的 16 元素分组使用两条指令;FP8 则使用一条。
942
943## 10. 三个常见追问
944
945### 10.1 所以替换只是为了改成静态布局?
946
947在当前紧凑示例中,是的:源 M stride 的数值已经是 D,替换只是将其从 `int` 改成 `Int<D>`,保持数值映射一致。
948
949更完整地说,它构造的是 copy 使用的静态参考布局,而不是修改真实源 tensor。
950
951### 10.2 `tSgQ` 为什么是动态布局?
952
953M stride 来自调用方的运行时参数,并且 mainloop 明确将其类型设成 `int`。`partition_A` 继承这个 stride,不会将运行时数值自动提升为类型常量。
954
955因此它是“静态 shape + 部分动态 stride”,不是 shape 也全动态。
956
957### 10.3 为什么不一开始直接使用静态 stride?
958
959可以重新设计参数转换,在源布局中直接使用模板 D;但这会把真实源行跨度绑定到模板配置上,而当前代码保留调用方传入的 head stride。
960
961从代码结构看,两者的职责不同:
962
963```text
964args.dQ:描述真实 Q 存储,作为运行时参数传入
965TileShape:描述 kernel tile,由模板确定
966```
967
968若第一项使用 `_0{}`,它的类型只表示常量 0,无法再赋值为调用方的 head stride。
969
970若直接使用 `Int<D>`,则是在源布局中用模板 D 覆盖调用方的 stride。当前示例恰好采用紧凑存储,两者一致;但保留运行时源 stride 与构造静态 copy 参考布局在职责上仍然不同。
971
972从布局映射看,若真实源行跨度记为 P,copy 仍可先按静态 D 生成逻辑坐标,再通过真实源布局得到:
973
974```text
975source_offset = m * P + d
976```
977
978这说明静态参考地址空间不必直接充当真实物理地址空间。这里讨论的是布局组合机制,并非对整个 kernel 的所有输入格式作支持承诺。
979
980## 11. 为什么是 `get<2>`,不是 `get<0>` 或 `get<1>`
981
982`TileShape` 的三维分别是 M、N、D:
983
984```text
985M:Q tile 行数
986N:KV 序列方向的 tile 大小
987D:Q/K 特征方向的 tile 大小
988```
989
990紧凑 Q 参考布局中,一行包含 D 个元素,所以 M 方向的 stride 必须表达 D:
991
992```text
993reference_offset = m * D + d
994```
995
996M 是行数,N 是 KV tile 宽度,都不是一行 Q 的特征数。本例中 M、N、D 恰好都为 128,不代表三者可以在语义上互换。例如一个 `64×128` 的紧凑行布局,其 M stride 是 128,而不是 64。
997
998`make_cotiled_copy` 本身并不认识 `head_dim`;是调用方用它定义了这个静态、紧凑的参考地址空间。
999
1000## 12. 应避免的错误理解
1001
1002| 错误理解 | 源码支持的解释 |
1003|---|---|
1004| 替换的是 `ThrV` stride | `partition_A` 已切掉线程 mode;替换的是 FrgV 内的 M stride |
1005| 原 M stride 为 0 | kernel 参数转换将它赋值为原 Q head stride,当前示例为 D |
1006| 128 是运行时已知值,所以布局静态 | CuTe 静态性由类型判断;普通 `int(128)` 仍是动态项 |
1007| `replace` 修改了 `tSgQ` 的访存布局 | 它返回新 tuple,用于构造参考 layout;真实源 tensor 未被修改 |
1008| copy 的 1024 个元素就是整个 Q tile | 一次分工为 1024 个元素,重复后覆盖 16384 个元素 |
1009| inverse 输出可以直接当物理地址 | inverse 输出是 data 坐标编号,后续还需转换为 tile 坐标 |
1010| decode 只加载第 0 行 | M 维承载 G 个 Q heads,掩码判定 `m<G` |
1011| 静态参考 layout 就是共享内存物理布局 | 目的 tensor 使用自身的 shared-memory layout 和 swizzle |
1012| prefill 的 256×128 配置适用于这段代码 | 本文加载器应沿 generation/decode 调用路径分析 |
1013
1014## 13. 验证范围
1015
1016本文具体布局来自本次会话中对实际 mainloop/builder 的 host 端实例化,FP8 和 FP16 均核对了:
1017
10181. 当前紧凑示例中,替换前后全部 Q tile 坐标的地址映射一致。
10192. `data_layout(layout_tv_data(t,v))` 恢复 `atom_q_tv(t,v)` 的参考地址。
10203. `tile2data(layout_tv(t,v))` 恢复 `layout_tv_data(t,v)` 的 data 坐标编号。
10214. 64 个线程的最终 partition 覆盖全部 `128×128` 元素,每个坐标恰好一次。
10225. 源 partition 与共享内存 partition 在同一 pipeline stage 上对应相同的逻辑 `(m,d)` 坐标。
1023
1024补充第 8.1 节时,另行枚举了 FP8、FP16 各 1024 个 thread/value 坐标,确认 `trg_layout_TV`、zip 后布局、profile coalesce 后布局对相应结构坐标的映射一致,并核对了不同 profile 的实际打印结果。
1025
1026这些检查验证的是类型、布局和坐标映射;没有执行 GPU kernel。
1027
1028## 14. 源码阅读顺序
1029
1030| 顺序 | 源码位置 | 要确认的问题 |
1031|---:|---|---|
1032| 1 | [77_blackwell_fmha_gen.cu:329](/home/x/workspace/cutlass/examples/77_blackwell_fmha/77_blackwell_fmha_gen.cu:329) | 输入类型、shape、stride 类型 |
1033| 2 | [77_blackwell_fmha_gen.cu:472](/home/x/workspace/cutlass/examples/77_blackwell_fmha/77_blackwell_fmha_gen.cu:472) | Q head stride 为什么等于 D |
1034| 3 | [sm100_fmha_gen_kernel_warpspecialized.hpp:205](/home/x/workspace/cutlass/examples/77_blackwell_fmha/kernel/sm100_fmha_gen_kernel_warpspecialized.hpp:205) | G 如何搬到 M,head stride 如何搬到 M stride |
1035| 4 | [sm100_fmha_gen_mainloop_warpspecialized.hpp:75](/home/x/workspace/cutlass/examples/77_blackwell_fmha/collective/sm100_fmha_gen_mainloop_warpspecialized.hpp:75) | 为什么 M stride 的类型是 int |
1036| 5 | [sm100_fmha_gen_mainloop_warpspecialized.hpp:97](/home/x/workspace/cutlass/examples/77_blackwell_fmha/collective/sm100_fmha_gen_mainloop_warpspecialized.hpp:97) | TileShapeQK 和 builder 的输入 |
1037| 6 | [mma_traits_sm100.hpp:2485](/home/x/workspace/cutlass/include/cute/atom/mma_traits_sm100.hpp:2485) | FP8 atom K、ThrID、ALayout |
1038| 7 | [mma_atom.hpp:291](/home/x/workspace/cutlass/include/cute/atom/mma_atom.hpp:291) | thrfrg_A 的层级结构 |
1039| 8 | [mma_atom.hpp:478](/home/x/workspace/cutlass/include/cute/atom/mma_atom.hpp:478) | partition_A 如何切掉线程 mode |
1040| 9 | [sm100_fmha_load_cpasync_warpspecialized.hpp:163](/home/x/workspace/cutlass/examples/77_blackwell_fmha/collective/sm100_fmha_load_cpasync_warpspecialized.hpp:163) | tSgQ、replace、copy partition 和掩码 |
1041| 10 | [copy_atom.hpp:528](/home/x/workspace/cutlass/include/cute/atom/copy_atom.hpp:528) | inverse、投影、tiler、tile2data、layout_tv |
1042| 11 | [layout.hpp:1364](/home/x/workspace/cutlass/include/cute/layout.hpp:1364) | left_inverse 的静态 stride 要求 |
1043| 12 | [copy_atom.hpp:365](/home/x/workspace/cutlass/include/cute/atom/copy_atom.hpp:365) | 静态分工如何作用到真实源和目的布局 |
1044