brandonmusic commited on
Commit
ad1f989
·
verified ·
1 Parent(s): d6106e7

Add fused-MoE patches for custom image builds

Browse files
patches/patch_sparkinfer_projection_tiers.py ADDED
@@ -0,0 +1,383 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Let SparkInfer's fused mixed-Trellis MoE tier per (expert, projection).
3
+
4
+ Target: sparkinfer from r28 (si200c1db), ABI v6.
5
+ Idempotent, exact anchors, fails closed.
6
+
7
+ WHY
8
+ R7 chooses K independently for every (expert, projection). The fused kernel
9
+ tiers per expert, so an expert that is K3 for gate and K4 for up cannot be
10
+ expressed and the whole checkpoint falls back to exllamav3's exl3_moe --
11
+ which measures 27% of HBM bandwidth and is 50% of a decode step.
12
+
13
+ Counting the checkpoint per layer, though:
14
+
15
+ distinct FC1 K values per layer (|Kg u Ku|): 2 -> 73 layers, 3 -> 2
16
+ distinct FC2 K values per layer (|Kd|): 2 -> 71 layers, 3 -> 4
17
+
18
+ Two bitrates, {3,4}, on both phases in nearly every layer. So tier0 = K3 and
19
+ tier1 = K4 already serves gate, up AND down; no per-phase trellis_bits is
20
+ needed. The only inexpressible thing is per-projection MEMBERSHIP, and that
21
+ is a runtime table:
22
+
23
+ descriptor = descriptor_map[combined_expert] # int32[total_experts]
24
+ tier = descriptor >> 8
25
+ local = descriptor & 0xFF
26
+
27
+ WHAT
28
+ descriptor_map grows from int32[total_experts] to int32[3 * total_experts],
29
+ rows ordered gate, up, down, same encoding. The dispatch picks a row:
30
+ FC2 is const_expr; FC1 branches on output_n_tile, which the tile already
31
+ holds and which trellis3_t256_proj guarantees is projection-aligned (it
32
+ rejects any geometry where a projection is not an integral number of CTA N
33
+ tiles). Row 0 is byte-compatible with the old single-row table, so a caller
34
+ that passes three identical rows reproduces current behaviour exactly.
35
+
36
+ No weight-stack surgery. local_expert is now per projection and the gate/up
37
+ half offset already rides in output_n_tile, so gate at tier-local slot i and
38
+ up at slot j address as i*expert_stride + gate_half and j*expert_stride +
39
+ up_half inside the existing w13 stack. Strides, scales, rotations and the
40
+ route packer are untouched; the stack sizes to max(gate_count, up_count).
41
+
42
+ CORRECTNESS
43
+ For any layer where gate and up share a tier assignment, all three rows are
44
+ identical and every launch resolves exactly as before -- that is the
45
+ bit-identical gate. Reduction order, fragment layout and accumulation are
46
+ untouched: this only changes which tier's B pointer a tile reads.
47
+ """
48
+ from __future__ import annotations
49
+
50
+ import argparse
51
+ from pathlib import Path
52
+
53
+ MARKER = "glm52-r7-projtiers"
54
+
55
+ MIXED = "sparkinfer/moe/_shared/kernels/w4a16/mixed_trellis.py"
56
+
57
+
58
+ def replace_once(text: str, old: str, new: str, label: str) -> str:
59
+ count = text.count(old)
60
+ if count != 1:
61
+ raise RuntimeError(f"{label}: expected one exact anchor, found {count}")
62
+ return text.replace(old, new, 1)
63
+
64
+
65
+ def patch_mixed(text: str) -> str:
66
+ # 1. Device tensor: three rows instead of one.
67
+ text = replace_once(
68
+ text,
69
+ """ descriptor_map = cute.make_tensor(
70
+ descriptor_map_ptr,
71
+ layout=cute.make_layout((total_experts,), stride=(1,)),
72
+ )
73
+ """,
74
+ """ # glm52-r7-projtiers: rows are gate, up, down. Row 0 alone is the
75
+ # historical layout, so three identical rows reproduce it exactly.
76
+ descriptor_map = cute.make_tensor(
77
+ descriptor_map_ptr,
78
+ layout=cute.make_layout(
79
+ (cutlass.Int64(3) * total_experts,), stride=(1,)
80
+ ),
81
+ )
82
+ """,
83
+ "descriptor_map tensor",
84
+ )
85
+
86
+ # 2. Dispatch: resolve the row before the lookup. FC2 is compile-time; FC1
87
+ # reads the half from output_n_tile, which is already a parameter here.
88
+ text = replace_once(
89
+ text,
90
+ """ combined_expert = block_expert_ids[metadata_block_idx].to(Int32)
91
+ total_experts = tier0_num_experts + tier1_num_experts
92
+ if combined_expert >= Int32(0) and combined_expert < total_experts:
93
+ descriptor = descriptor_map[combined_expert].to(Int32)
94
+ """,
95
+ """ combined_expert = block_expert_ids[metadata_block_idx].to(Int32)
96
+ total_experts = tier0_num_experts + tier1_num_experts
97
+ # glm52-r7-projtiers: gate and up may sit in different tiers, so the
98
+ # descriptor row is chosen per projection. FC2 resolves at compile time;
99
+ # FC1 splits on the N half, which trellis3_t256_proj keeps aligned to
100
+ # whole CTA N tiles.
101
+ descriptor_row = Int32(2)
102
+ if cutlass.const_expr(is_fc1):
103
+ fc1_half_tiles = Int32(self.driver.fc1.n_tiles // 2)
104
+ descriptor_row = Int32(0)
105
+ if output_n_tile >= fc1_half_tiles:
106
+ descriptor_row = Int32(1)
107
+ if combined_expert >= Int32(0) and combined_expert < total_experts:
108
+ descriptor = descriptor_map[
109
+ descriptor_row * total_experts + combined_expert
110
+ ].to(Int32)
111
+ """,
112
+ "per-projection descriptor row",
113
+ )
114
+
115
+ # 3. Host validation: the table is three rows now.
116
+ text = replace_once(
117
+ text,
118
+ """ total_experts = launch.tier0_num_experts + launch.tier1_num_experts
119
+ for name, mapping in (
120
+ ("global_to_combined", global_to_combined),
121
+ ("descriptor_map", descriptor_map),
122
+ ):
123
+ if (
124
+ mapping.dtype != torch.int32
125
+ or mapping.device != x.device
126
+ or not mapping.is_contiguous()
127
+ or int(mapping.numel()) != total_experts
128
+ ):
129
+ """,
130
+ """ total_experts = launch.tier0_num_experts + launch.tier1_num_experts
131
+ for name, mapping in (
132
+ ("global_to_combined", global_to_combined),
133
+ ("descriptor_map", descriptor_map),
134
+ ):
135
+ # glm52-r7-projtiers: descriptor_map carries one row per projection.
136
+ expected_entries = (
137
+ 3 * total_experts if name == "descriptor_map" else total_experts
138
+ )
139
+ if (
140
+ mapping.dtype != torch.int32
141
+ or mapping.device != x.device
142
+ or not mapping.is_contiguous()
143
+ or int(mapping.numel()) != expected_entries
144
+ ):
145
+ """,
146
+ "descriptor_map validation",
147
+ )
148
+
149
+ # 4. FC1 and FC2 hold different expert counts once tiering is per
150
+ # projection: on this checkpoint tier0 (K3) carries ~180 gate/up planes
151
+ # but ~25 down planes, because down is 87% K4. Equalising by padding
152
+ # costs 49.8 GiB per rank on the real bit maps, so the counts must be
153
+ # allowed to differ. The FC2 count is read back from the tier's own
154
+ # w2_global_scale, which is one fp32 per FC2 expert, so no launch-plan
155
+ # or kernel-signature change is needed. The kernel's flat layouts stay
156
+ # sized by the FC1 count: they are pointer arithmetic, not bounds checks,
157
+ # and descriptor row 2 only ever emits FC2-local indices below the real
158
+ # count, so no access lands past the allocation.
159
+ text = replace_once(
160
+ text,
161
+ """ expected_experts = int(expected_experts)
162
+ bits = int(bits)
163
+ fc1_cols = 2 * int(intermediate_size)
164
+ """,
165
+ """ expected_experts = int(expected_experts)
166
+ # glm52-r7-projtiers
167
+ fc2_experts = int(tier.w2_global_scale.numel())
168
+ bits = int(bits)
169
+ fc1_cols = 2 * int(intermediate_size)
170
+ """,
171
+ "fc2 expert count",
172
+ )
173
+ text = replace_once(
174
+ text,
175
+ """ (
176
+ "w2",
177
+ tier.w2,
178
+ torch.int32,
179
+ expected_experts
180
+ * (int(intermediate_size) // 16)
181
+ * (int(hidden_size) // 16)
182
+ * (8 * bits),
183
+ ),
184
+ """,
185
+ """ (
186
+ "w2",
187
+ tier.w2,
188
+ torch.int32,
189
+ fc2_experts # glm52-r7-projtiers
190
+ * (int(intermediate_size) // 16)
191
+ * (int(hidden_size) // 16)
192
+ * (8 * bits),
193
+ ),
194
+ """,
195
+ "w2 storage extent",
196
+ )
197
+ text = replace_once(
198
+ text,
199
+ """ (
200
+ "w2_global_scale",
201
+ tier.w2_global_scale,
202
+ torch.float32,
203
+ expected_experts,
204
+ ),
205
+ """,
206
+ """ (
207
+ "w2_global_scale",
208
+ tier.w2_global_scale,
209
+ torch.float32,
210
+ fc2_experts, # glm52-r7-projtiers
211
+ ),
212
+ """,
213
+ "w2 global scale extent",
214
+ )
215
+
216
+ # 5. The launch count is now an upper bound, not an exact count: with
217
+ # per-projection tiering FC1 and FC2 hold different expert counts, and
218
+ # the kernel guards dispatch with `local_expert < tierN_num_experts`, so
219
+ # the count must cover the LARGER phase. The prepared tier still reports
220
+ # its FC1 count, so require only that it fits inside the plan.
221
+ text = replace_once(
222
+ text,
223
+ """ actual_experts = int(tier.num_experts)
224
+ if actual_experts != int(expected_experts):
225
+ raise ValueError(
226
+ f"mixed Trellis {name} has {actual_experts} experts, but the "
227
+ f"launch plan describes {int(expected_experts)}"
228
+ )
229
+ """,
230
+ """ actual_experts = int(tier.num_experts)
231
+ if actual_experts > int(expected_experts): # glm52-r7-projtiers
232
+ raise ValueError(
233
+ f"mixed Trellis {name} has {actual_experts} experts, which "
234
+ f"exceeds the {int(expected_experts)} the launch plan allows"
235
+ )
236
+ """,
237
+ "tier expert-count bound",
238
+ )
239
+
240
+
241
+ # 7. The `local_expert < tierN_num_experts` guard uses ONE count per tier,
242
+ # which is the FC1 slot count because w13 storage is validated against
243
+ # it. Under per-projection tiering FC2 can hold more experts than FC1
244
+ # (tier1: 231 down vs 77 gate/up), so that bound wrongly rejects most
245
+ # down-projection tiles and silently drops their output -- measured as
246
+ # KLD 2.37 against a 0.0625 control. The descriptor map only ever emits
247
+ # FC2 locals below the real FC2 count, so the bound is redundant there.
248
+ text = replace_once(
249
+ text,
250
+ """ if tier == Int32(0) and local_expert < tier0_num_experts:""",
251
+ """ fc1_bound_ok = local_expert < tier0_num_experts
252
+ if cutlass.const_expr(not is_fc1):
253
+ fc1_bound_ok = True # glm52-r7-projtiers
254
+ if tier == Int32(0) and fc1_bound_ok:""",
255
+ "tier0 fc2 bound",
256
+ )
257
+ text = replace_once(
258
+ text,
259
+ """ elif tier == Int32(1) and local_expert < tier1_num_experts:""",
260
+ """ fc1_bound_ok1 = local_expert < tier1_num_experts
261
+ if cutlass.const_expr(not is_fc1):
262
+ fc1_bound_ok1 = True # glm52-r7-projtiers
263
+ if tier == Int32(1) and fc1_bound_ok1:""",
264
+ "tier1 fc2 bound",
265
+ )
266
+
267
+ # 6. Host builder for the three-row table.
268
+ text = replace_once(
269
+ text,
270
+ """def combine_trellis_rotations(""",
271
+ '''def build_projection_tiered_maps(
272
+ gate_tiers: Sequence[int],
273
+ up_tiers: Sequence[int],
274
+ down_tiers: Sequence[int],
275
+ *,
276
+ tier_slots: Sequence[int],
277
+ device: torch.device,
278
+ ) -> tuple[torch.Tensor, torch.Tensor]:
279
+ """Build the route map and the three-row descriptor map for one R7 layer.
280
+
281
+ Each argument is one tier id (0 or 1) per global expert, for that
282
+ projection. Combined expert ids are the global ids -- with per-projection
283
+ tiering there is no single tier-contiguous ordering, so all tier knowledge
284
+ lives in the descriptor rows and global_to_combined is the identity.
285
+
286
+ Returns (global_to_combined, descriptor_map) where descriptor_map is
287
+ int32[3 * num_experts] laid out gate, up, down, each entry
288
+ (tier << 8) | tier_local_index. glm52-r7-projtiers.
289
+ """
290
+
291
+ projections = (
292
+ ("gate", tuple(int(t) for t in gate_tiers)),
293
+ ("up", tuple(int(t) for t in up_tiers)),
294
+ ("down", tuple(int(t) for t in down_tiers)),
295
+ )
296
+ num_experts = len(projections[0][1])
297
+ rows: list[int] = []
298
+ for name, tiers in projections:
299
+ if len(tiers) != num_experts:
300
+ raise ValueError(
301
+ "mixed Trellis projection tier lists must agree on expert count: "
302
+ f"{name} has {len(tiers)}, expected {num_experts}"
303
+ )
304
+ if any(t not in (0, 1) for t in tiers):
305
+ raise ValueError(f"mixed Trellis {name} tier ids must be 0 or 1")
306
+ counters = [0, 0]
307
+ row = []
308
+ for tier in tiers:
309
+ local = counters[tier]
310
+ counters[tier] += 1
311
+ if local > 0xFF:
312
+ raise ValueError(
313
+ f"mixed Trellis {name} tier {tier} exceeds 256 experts"
314
+ )
315
+ row.append((tier << 8) | local)
316
+ rows.extend(row)
317
+ # The launch sizes the combined namespace as the sum of the tier slot
318
+ # counts, and a tier slots max(gate_count, up_count), so with per-projection
319
+ # tiering that sum exceeds the real expert count (257 vs 256 on this
320
+ # checkpoint). Both tables are laid out on the padded stride; the tail
321
+ # entries are -1 and the kernel skips them, since it guards on
322
+ # 0 <= combined_expert < total and descriptor >= 0.
323
+ stride = int(sum(int(v) for v in tier_slots))
324
+ if stride < num_experts:
325
+ raise ValueError(
326
+ f"mixed Trellis tier slots ({stride}) cannot address "
327
+ f"{num_experts} experts"
328
+ )
329
+ global_to_combined = torch.full(
330
+ (stride,), -1, dtype=torch.int32, device=device
331
+ )
332
+ global_to_combined[:num_experts] = torch.arange(
333
+ num_experts, dtype=torch.int32, device=device
334
+ )
335
+ descriptor = torch.full((3 * stride,), -1, dtype=torch.int32, device=device)
336
+ for row_index in range(3):
337
+ base = row_index * num_experts
338
+ descriptor[row_index * stride : row_index * stride + num_experts] = (
339
+ torch.tensor(
340
+ rows[base : base + num_experts], dtype=torch.int32, device=device
341
+ )
342
+ )
343
+ return global_to_combined, descriptor
344
+
345
+
346
+ def combine_trellis_rotations(''',
347
+ "projection map builder",
348
+ )
349
+ return text
350
+
351
+
352
+ PATCHERS = {MIXED: patch_mixed}
353
+
354
+
355
+ def main() -> int:
356
+ parser = argparse.ArgumentParser()
357
+ parser.add_argument(
358
+ "--root", default="/opt/venv/lib/python3.12/site-packages", type=Path
359
+ )
360
+ parser.add_argument("--check", action="store_true")
361
+ args = parser.parse_args()
362
+
363
+ staged = {}
364
+ for relative, patcher in PATCHERS.items():
365
+ target = args.root / relative
366
+ text = target.read_text()
367
+ if MARKER in text:
368
+ print(f"already patched: {relative}")
369
+ return 0
370
+ staged[target] = patcher(text)
371
+ print(f"ready: {relative}")
372
+ if args.check:
373
+ print("sparkinfer projection tiers: anchors valid; not applied")
374
+ return 0
375
+ for target, text in staged.items():
376
+ target.write_text(text)
377
+ compile(text, str(target), "exec")
378
+ print("sparkinfer projection tiers: applied")
379
+ return 0
380
+
381
+
382
+ if __name__ == "__main__":
383
+ raise SystemExit(main())