Skip to content

Commit ece6342

Browse files
fsx950223claude
andcommitted
[FlyDSL] [Perf] pa_decode: launch gfx942 query-split CTAs split-major
The tile grid is (plan capacity, KV heads x query splits). Dispatch walks x first, so split 1 of task 0 dispatches only after every slot of split 0, padding slots included. On small grids, where the plan has many more slots than tasks, the splits ran in sequential waves of mostly empty CTAs. On large grids a task's splits ran far apart, each re-reading the task's K/V from HBM. On gfx942, split kernels now launch (KV heads x query splits, capacity), so a task's splits dispatch together. They run concurrently and share the K/V they re-read. The cache key records the choice. BF16 splits forced by the LDS limit at large head dims only use it when tasks own at least four tiles; with fewer, the split-major grid measured 0.93-0.97x. Paired in-process A/B vs 8f9fe39 on MI308X (gfx942): - 180 MTP grid cases: geomean 1.092x - FP8 MTP4/8 batch 1: 1.04-1.45x - FP8 sliding-window MTP4 batch 1/16: 1.28-1.63x - BF16 D1024 MTP batch >= 16 with ctx >= 8K: 1.10-1.26x - 111 unit-test cases: geomean 1.018x (MTP4/8 and window cases 1.16-1.20x) gfx950 binaries are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 8f9fe39 commit ece6342

3 files changed

Lines changed: 34 additions & 6 deletions

File tree

‎aiter/ops/flydsl/kernels/pa_decode/context.py‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,16 @@ def init_thread_mapping(self):
6666
self.warp = self.tid // WAVE # 0..NWARP-1
6767
self.lane = self.tid - self.warp * WAVE # 0..63
6868
self.seq = self.planned_seq
69-
self.kv_query = fx.Int32(gpu.block_id("y"))
69+
# A split-major grid puts (KV head, query split) on x, the slot on y.
70+
qx, px = ("x", "y") if self.traits.SPLIT_MAJOR else ("y", "x")
71+
self.kv_query = fx.Int32(gpu.block_id(qx))
7072
self.kv_h = self.kv_query // self.traits.query_splits
7173
self.query_begin = (
7274
self.kv_query % self.traits.query_splits
7375
) * self.traits.QUERIES_PER_CTA
74-
self.n_kv = fx.Int32(gpu.grid_dim.y) // self.traits.query_splits
75-
self.part = fx.Int32(gpu.block_id("x"))
76-
self.partial_slot = self.kv_h * fx.Int32(gpu.grid_dim.x) + self.part
76+
self.n_kv = fx.Int32(getattr(gpu.grid_dim, qx)) // self.traits.query_splits
77+
self.part = fx.Int32(gpu.block_id(px))
78+
self.partial_slot = self.kv_h * fx.Int32(getattr(gpu.grid_dim, px)) + self.part
7779

7880
def init_query_mapping(self):
7981
self.rgroup = (

‎aiter/ops/flydsl/kernels/pa_decode/traits.py‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,24 @@ def select(
451451
or (single_tile_plan and split_workgroups <= 2 * num_compute_units)
452452
)
453453
)
454+
# gfx942 split CTAs launch split-major, so a task's splits dispatch
455+
# together instead of after every slot of the previous split. BF16
456+
# head-dim splits forced by LDS only gain when tasks are long.
457+
split_major = (
458+
"gfx94" in arch
459+
and query_splits > 1
460+
and (
461+
kv_dtype in FP8_KV_DTYPES
462+
or head_dim <= 256
463+
or (
464+
max_context_length is not None
465+
and max_tiles_per_task(
466+
num_seqs, work_capacity, num_partitions, max_context_length
467+
)
468+
>= 4
469+
)
470+
)
471+
)
454472
cache_key = (
455473
head_dim,
456474
query_group_size,
@@ -472,6 +490,7 @@ def select(
472490
bool(mask_unowned_kv),
473491
direct_output,
474492
gfx942_busy,
493+
split_major,
475494
D256_PIPE,
476495
stage_pages,
477496
)
@@ -532,6 +551,7 @@ class PaDecodeTraits:
532551
WAVES_PER_EU_2: bool
533552
GFX942_Q_EARLY: bool
534553
PAGES_EARLY: bool
554+
SPLIT_MAJOR: bool
535555
REUSE_KV_PAGES: bool
536556
SCALES_BEFORE_CURRENT_V: bool
537557
M1_SCALE_BEFORE_MASK: bool
@@ -616,6 +636,7 @@ def create(cls, schedule: PaDecodeSchedule) -> PaDecodeTraits:
616636
mask_unowned_kv,
617637
DIRECT,
618638
gfx942_busy,
639+
SPLIT_MAJOR,
619640
D256_PIPE,
620641
stage_page_capacity,
621642
) = cache_key
@@ -927,6 +948,7 @@ def create(cls, schedule: PaDecodeSchedule) -> PaDecodeTraits:
927948
or (M_TILES == 2 and kv_dtype in FP8_KV_DTYPES)
928949
)
929950
),
951+
SPLIT_MAJOR=SPLIT_MAJOR,
930952
GFX942_Q_EARLY=(
931953
not is_gfx950 and kv_dtype != MXFP4_KV_DTYPE and not STREAM_KV
932954
),

‎aiter/ops/flydsl/kernels/pa_decode_kernel.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ def _run_task(seq, start, end, context):
441441

442442
# Skip cleared padding records: no scratch writes or Q loads.
443443
# This CTA-uniform guard keeps every task barrier convergent.
444-
slot = fx.Int32(gpu.block_id("x"))
444+
slot = fx.Int32(gpu.block_id("y" if traits.SPLIT_MAJOR else "x"))
445445
work = fx.recast_iter(fx.Int32, work_info_ptr)
446446
if const_expr(traits.DIRECT):
447447
# One task per sequence, no plan: ``work_info`` is the context lengths.
@@ -517,7 +517,11 @@ def pa_decode_tile_launch(
517517
work_info,
518518
num_seqs,
519519
).launch(
520-
grid=(work_capacity, num_kv_heads * traits.query_splits, 1),
520+
grid=(
521+
(num_kv_heads * traits.query_splits, work_capacity, 1)
522+
if traits.SPLIT_MAJOR
523+
else (work_capacity, num_kv_heads * traits.query_splits, 1)
524+
),
521525
block=(traits.BLOCK_THREADS, 1, 1),
522526
stream=stream,
523527
)

0 commit comments

Comments
 (0)