From 5cf9474718c04525fab906577c38213fa09e7ae7 Mon Sep 17 00:00:00 2001 From: Taimuraz Kaitmazov Date: Fri, 26 Jun 2026 20:15:36 +0300 Subject: [PATCH 1/6] gemv: coalesce batched DMA into one iterated BD per column (default) The batched GEMV unrolled num_batches host DMA descriptors per column (one fill + one drain + a task-group wait per batch). Express the batch as a single iterated BD instead, the same B-unroll -> BD-iteration idiom GEMM already uses for tiling: place num_batches in the size-uncapped descriptor dim and split the contiguous run across the two wrap dims (each <= 1023), per the AIE shim BD limits (AIEXDialect.cpp verifyStridesWraps). The run split keeps the inner size and strides 4-byte-aligned (granularity-aware), and the per-batch drain wait is dropped in favour of ObjectFifo backpressure (fifo depth >= 2 asserted; the A-fill and C-drain run on separate shim channels, paced by the fifo locks). num_batches == 1 and any config that cannot be coalesced (run with no aligned split under the wrap cap, or a batch stride that is too large or unaligned) fall back to the existing per-batch path and are byte-identical. The coalesced descriptor accesses the exact same DRAM elements in the same order as the unroll (access-equivalent), so this is a descriptor-count / build-time and correctness change, not a runtime change. (cherry picked from commit d4bde9761803cea416423ae6cc2697db0b62e555) --- iron/operators/gemv/design.py | 92 +++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/iron/operators/gemv/design.py b/iron/operators/gemv/design.py index 654dc7f6..dbfc2dad 100644 --- a/iron/operators/gemv/design.py +++ b/iron/operators/gemv/design.py @@ -165,6 +165,70 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec): for col in range(cols) ] + # --- Batch-coalesce (default): one BD per column over all batches. --- + # Replaces the per-batch unroll with a single iterated BD; the stock A_taps/C_taps + # above remain the fallback (and the num_batches==1 path). Access-equivalent to the + # unroll (covered by test_gemv_batched). + # AIE shim BD limits (mlir-aie AIEXDialect.cpp verifyStridesWraps): the two wrap + # dims have a size cap (1023) while one dim is size-uncapped; TAP sizes are + # outermost-first and the verifier reverses them, so [1, num_batches, run_hi, + # run_lo] places num_batches in the uncapped dim and the contiguous run in the two + # wrap dims. Access-equivalent to the unroll (covered by test_gemv_batched). + # The shim also enforces a 4-byte address granularity on every transfer size and + # stride (NOT skipped, even for linear transfers); for bf16 (2 bytes) that means + # the innermost size (run_lo) and the batch stride must be even, so split_run only + # yields an even run_lo and the predicate requires even strides. + MAX_WRAP = 1023 + MAX_STRIDE = (1 << 20) - 1 # conservative element-stride bound for the wrap dims + GRAN_ELEMS = 2 # 4-byte shim granularity / 2-byte bf16 element + + def split_run(run, lim=MAX_WRAP, gran=GRAN_ELEMS): + """Factor a contiguous run into (hi, lo), both <= lim and lo a multiple of gran + (the address-granularity-aligned inner size), lo maximal. None if no such + split exists (caller then falls back to the per-batch path).""" + lo_start = (lim // gran) * gran + for lo in range(lo_start, 0, -gran): + if run % lo == 0 and (run // lo) <= lim: + return (run // lo, lo) + return None + + A_run, A_bstride = (M // cols) * K, M * K + C_run, C_bstride = (M // cols), M + A_split, C_split = split_run(A_run), split_run(C_run) + coalesce = ( + num_batches > 1 + and A_bstride <= MAX_STRIDE + and C_bstride <= MAX_STRIDE + and A_bstride % GRAN_ELEMS == 0 + and C_bstride % GRAN_ELEMS == 0 + and A_split is not None + and C_split is not None + ) + + def coalesced_tap(L3_ty, col_off, split, bstride): + run_hi, run_lo = split + return TensorAccessPattern( + tensor_dims=L3_ty.__args__[0], + offset=col_off, + sizes=[1, num_batches, run_hi, run_lo], + strides=[0, bstride, run_lo, 1], + ) + + if coalesce: + # Backpressure replaces the per-batch drain wait, so the A/C ObjectFifos must + # be deep enough (>=2) for the producer not to overrun the consumer. + assert all(f.depth >= 2 for f in A_L3L1_fifos) and all( + f.depth >= 2 for f in C_L1L3_fifos + ), "coalesced GEMV needs A/C ObjectFifo depth>=2 (replaces the per-batch wait)" + A_taps_coalesced = [ + coalesced_tap(L3_A_ty, col * (M // cols) * K, A_split, A_bstride) + for col in range(cols) + ] + C_taps_coalesced = [ + coalesced_tap(L3_C_ty, col * (M // cols), C_split, C_bstride) + for col in range(cols) + ] + rt = Runtime() with rt.sequence(L3_A_ty, L3_B_ty, L3_C_ty) as (A, B, C): rt.start(*workers) @@ -172,21 +236,43 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec): for col in range(cols): # Simple linear transfer of B, includes all batches in sequence rt.fill(B_L3L1_fifos[col].prod(), B, B_tap, task_group=tg_b) - for batch in range(num_batches): + if coalesce: + # One iterated BD per column over all batches; per-batch `wait` dropped + # in favor of ObjectFifo backpressure (asserted above). tg_ac = rt.task_group() for col in range(cols): rt.fill( - A_L3L1_fifos[col].prod(), A, A_taps[col][batch], task_group=tg_ac + A_L3L1_fifos[col].prod(), A, A_taps_coalesced[col], task_group=tg_ac ) for col in range(cols): rt.drain( C_L1L3_fifos[col].cons(), C, - C_taps[col][batch], + C_taps_coalesced[col], task_group=tg_ac, wait=True, ) rt.finish_task_group(tg_ac) + else: + # Fallback (also the num_batches==1 path): stock per-batch unroll. + for batch in range(num_batches): + tg_ac = rt.task_group() + for col in range(cols): + rt.fill( + A_L3L1_fifos[col].prod(), + A, + A_taps[col][batch], + task_group=tg_ac, + ) + for col in range(cols): + rt.drain( + C_L1L3_fifos[col].cons(), + C, + C_taps[col][batch], + task_group=tg_ac, + wait=True, + ) + rt.finish_task_group(tg_ac) rt.finish_task_group(tg_b) return Program(dev, rt).resolve_program() From 72940a289a5436cbdc8e830d7b3bd533039e30f4 Mon Sep 17 00:00:00 2001 From: Taimuraz Kaitmazov Date: Fri, 26 Jun 2026 20:15:36 +0300 Subject: [PATCH 2/6] gemv: add num_batches > 1 golden tests The test suite had no num_batches > 1 GEMV coverage. Add a batched golden reference (num_batches independent matrix-vector products, stacked contiguously) and a parametrized device test covering: the coalesced path with large num_batches (the size-uncapped dim) and a multi-dimension run split; a run that requires an aligned (even) inner split; and the per-batch fallback (batch stride over the limit). (cherry picked from commit 6d9a84526a684d8df9b46c43ef8599a3af70b557) --- iron/operators/gemv/reference.py | 24 +++++++++++++++ iron/operators/gemv/test.py | 53 +++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/iron/operators/gemv/reference.py b/iron/operators/gemv/reference.py index dc72ae96..b7d8d821 100644 --- a/iron/operators/gemv/reference.py +++ b/iron/operators/gemv/reference.py @@ -40,3 +40,27 @@ def generate_golden_reference( "B": B, "C": C, } + + +def generate_golden_reference_batched(M=128, K=128, num_batches=2, seed=42): + """ + Generate golden reference data for a batched GEMV (num_batches independent + matrix-vector products stacked contiguously, matching the GEMV op layout). + + Parameters: + M: Number of rows of each matrix A + K: Number of columns of each matrix A (equals vector B length) + num_batches: Number of independent GEMVs + seed: Random seed + + Returns: + dict: Contains 'A' (matrices), 'B' (vectors), 'C' (output vectors) + """ + torch.manual_seed(seed) + val_range = 4 + A = torch.randn(num_batches, M, K, dtype=torch.bfloat16) * val_range + B = torch.randn(num_batches, K, dtype=torch.bfloat16) * val_range + C = torch.empty(num_batches, M, dtype=torch.bfloat16) + for b in range(num_batches): + C[b] = A[b] @ B[b] + return {"A": A, "B": B, "C": C} diff --git a/iron/operators/gemv/test.py b/iron/operators/gemv/test.py index 2abb42c0..d4a46c47 100755 --- a/iron/operators/gemv/test.py +++ b/iron/operators/gemv/test.py @@ -6,7 +6,10 @@ import aie.utils as aie_utils from iron.operators.gemv.op import GEMV -from iron.operators.gemv.reference import generate_golden_reference +from iron.operators.gemv.reference import ( + generate_golden_reference, + generate_golden_reference_batched, +) from iron.common.test_utils import run_test @@ -69,3 +72,51 @@ def test_gemv(M, K, num_aie_columns, tile_size_input, tile_size_output, aie_cont print(f"Effective Bandwidth: {bandwidth_gbps:.6e} GB/s\n") assert not errors, f"Test failed with errors: {errors}" + + +def get_batched_params(): + max_cols = aie_utils.get_current_device().cols + # (M, K, cols, tsi, tso, num_batches): exercise the coalesced path + fallback. + plist = [ + (256, 128, 1, 1, 256, 4), # tiny, coalesced + (256, 128, 8, 1, 32, 100), # large num_batches -> the size-uncapped dim + (448, 64, 8, 1, 56, 192), # multi-dim run split + large num_batches together + (64, 1536, 1, 1, 64, 8), # large K + (1026, 64, 1, 1, 2, 2), # run needs an even (granularity-aligned) split + (1024, 1024, 1, 1, 64, 2), # batch stride > 2**20 -> falls back to per-batch + (512, 64, 8, 4, 64, 32), # attn-style: tile_size_input>1, num_batches=heads + ] + out = [] + for p in plist: + if p[2] > max_cols: + continue + out.append(pytest.param(*p)) + return out + + +@pytest.mark.parametrize( + "M,K,num_aie_columns,tile_size_input,tile_size_output,num_batches", + get_batched_params(), +) +def test_gemv_batched( + M, K, num_aie_columns, tile_size_input, tile_size_output, num_batches, aie_context +): + golden = generate_golden_reference_batched(M=M, K=K, num_batches=num_batches) + operator = GEMV( + M=M, + K=K, + num_aie_columns=num_aie_columns, + tile_size_input=tile_size_input, + tile_size_output=tile_size_output, + num_batches=num_batches, + context=aie_context, + ) + input_buffers = { + "matrix": golden["A"].flatten(), + "vector": golden["B"].flatten(), + } + output_buffers = {"output": golden["C"].flatten()} + errors, *_ = run_test( + operator, input_buffers, output_buffers, rel_tol=0.04, abs_tol=1e-3 + ) + assert not errors, f"batched GEMV failed: {errors}" From 95d1c679bfbdc8b094fc5206f9a75511d9fcbd5a Mon Sep 17 00:00:00 2001 From: Taimuraz Kaitmazov Date: Tue, 30 Jun 2026 14:17:43 +0300 Subject: [PATCH 3/6] gemv: address review (explain non-linear TAP, fix backpressure comment, dedup branches) Responds to andrej's review on #127: - Explain the coalesced TAP is NOT a single linear transfer: contiguous within a batch but strided by the full matrix across batches (M*K != run for cols>1), so the batch iteration dim and the TAP are required. The inner run-split is only to fit the 1023 wrap cap; note it is removable once IRON's mlir_aie pin moves past mlir-aie #3036. - Add FIXME to source the shim BD bounds from the MLIR-AIE target model (AIEXDialect.cpp). - Reword the depth>=2 rationale: ObjectFifo backpressure blocks (never overruns/corrupts); depth>=2 is a performance/overlap guard, not correctness. - Dedup the coalesced/fallback branches into one loop over num_waits (1 vs num_batches). --- iron/operators/gemv/design.py | 81 ++++++++++++++++++----------------- iron/operators/gemv/test.py | 13 +++++- 2 files changed, 54 insertions(+), 40 deletions(-) diff --git a/iron/operators/gemv/design.py b/iron/operators/gemv/design.py index dbfc2dad..619e30b0 100644 --- a/iron/operators/gemv/design.py +++ b/iron/operators/gemv/design.py @@ -169,15 +169,31 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec): # Replaces the per-batch unroll with a single iterated BD; the stock A_taps/C_taps # above remain the fallback (and the num_batches==1 path). Access-equivalent to the # unroll (covered by test_gemv_batched). - # AIE shim BD limits (mlir-aie AIEXDialect.cpp verifyStridesWraps): the two wrap - # dims have a size cap (1023) while one dim is size-uncapped; TAP sizes are - # outermost-first and the verifier reverses them, so [1, num_batches, run_hi, - # run_lo] places num_batches in the uncapped dim and the contiguous run in the two - # wrap dims. Access-equivalent to the unroll (covered by test_gemv_batched). - # The shim also enforces a 4-byte address granularity on every transfer size and - # stride (NOT skipped, even for linear transfers); for bf16 (2 bytes) that means - # the innermost size (run_lo) and the batch stride must be even, so split_run only - # yields an even run_lo and the predicate requires even strides. + # + # This is NOT a single linear transfer. Within one batch the run is contiguous + # (A_run = (M//cols)*K elements), but the batch stride is the full matrix + # (A_bstride = M*K), so for cols>1 each column gathers its own slice out of every + # batch with a gap in between. Only cols==1 degenerates to bstride==run. So the + # batch dim is a genuine size-uncapped iteration dim and the TAP is required. + # + # The contiguous run is then split into two wrap dims [run_hi, run_lo] ONLY to fit + # the AIE shim's 10-bit (1023) wrap-size cap. TAP sizes are outermost-first and the + # verifier reverses them, so [1, num_batches, run_hi, run_lo] puts num_batches in the + # size-uncapped dim and the contiguous run in the two capped wrap dims. The shim also + # enforces a 4-byte address granularity on every size and stride (not skipped, even + # for linear transfers); for bf16 (2 bytes) that means run_lo and the batch stride + # must be even, so split_run only yields an even run_lo and the predicate requires + # even strides. + # + # FUTURE: this manual split is only needed on the current mlir_aie pin. Once IRON's + # pin moves past Xilinx/mlir-aie #3036 (LinearizeContiguousBDTransfer for the + # iteration dim, on top of #2924 which canonicalizes a contiguous run to linear form + # and bypasses the 1023 cap via the hardware buffer-length register), split_run / + # MAX_WRAP / GRAN_ELEMS can be dropped and the run supplied as one inner dim + # [num_batches, A_run]. The pin is currently frozen at the last pre-#3016 release. + # FIXME: pull these shim BD bounds from the MLIR-AIE target model rather than + # hard-coding them; they live in verifyStridesWraps in + # https://github.com/Xilinx/mlir-aie/blob/main/lib/Dialect/AIEX/IR/AIEXDialect.cpp MAX_WRAP = 1023 MAX_STRIDE = (1 << 20) - 1 # conservative element-stride bound for the wrap dims GRAN_ELEMS = 2 # 4-byte shim granularity / 2-byte bf16 element @@ -215,11 +231,15 @@ def coalesced_tap(L3_ty, col_off, split, bstride): ) if coalesce: - # Backpressure replaces the per-batch drain wait, so the A/C ObjectFifos must - # be deep enough (>=2) for the producer not to overrun the consumer. + # Dropping the per-batch drain wait lets the single iterated fill BD run ahead of + # the core. ObjectFifo lock backpressure keeps that safe: a producer that gets + # ahead BLOCKS on the buffer lock (worst case a stall, never a corrupting + # overrun). depth>=2 only buys OVERLAP of fill with compute, so it is a + # performance guard here, not a correctness requirement (depth==1 is correct but + # fully serial). assert all(f.depth >= 2 for f in A_L3L1_fifos) and all( f.depth >= 2 for f in C_L1L3_fifos - ), "coalesced GEMV needs A/C ObjectFifo depth>=2 (replaces the per-batch wait)" + ), "coalesced GEMV wants A/C ObjectFifo depth>=2 for fill/compute overlap" A_taps_coalesced = [ coalesced_tap(L3_A_ty, col * (M // cols) * K, A_split, A_bstride) for col in range(cols) @@ -236,43 +256,26 @@ def coalesced_tap(L3_ty, col_off, split, bstride): for col in range(cols): # Simple linear transfer of B, includes all batches in sequence rt.fill(B_L3L1_fifos[col].prod(), B, B_tap, task_group=tg_b) - if coalesce: - # One iterated BD per column over all batches; per-batch `wait` dropped - # in favor of ObjectFifo backpressure (asserted above). + # Coalesced: one iterated BD per column covers all batches (num_waits==1, a + # single drain wait for the whole column). Fallback (incl. num_batches==1): the + # stock per-batch unroll (num_waits==num_batches, one wait per batch). The fills + # and drains are otherwise identical; only the TAP and the wait count differ. + num_waits = 1 if coalesce else num_batches + for w in range(num_waits): tg_ac = rt.task_group() for col in range(cols): - rt.fill( - A_L3L1_fifos[col].prod(), A, A_taps_coalesced[col], task_group=tg_ac - ) + a_tap = A_taps_coalesced[col] if coalesce else A_taps[col][w] + rt.fill(A_L3L1_fifos[col].prod(), A, a_tap, task_group=tg_ac) for col in range(cols): + c_tap = C_taps_coalesced[col] if coalesce else C_taps[col][w] rt.drain( C_L1L3_fifos[col].cons(), C, - C_taps_coalesced[col], + c_tap, task_group=tg_ac, wait=True, ) rt.finish_task_group(tg_ac) - else: - # Fallback (also the num_batches==1 path): stock per-batch unroll. - for batch in range(num_batches): - tg_ac = rt.task_group() - for col in range(cols): - rt.fill( - A_L3L1_fifos[col].prod(), - A, - A_taps[col][batch], - task_group=tg_ac, - ) - for col in range(cols): - rt.drain( - C_L1L3_fifos[col].cons(), - C, - C_taps[col][batch], - task_group=tg_ac, - wait=True, - ) - rt.finish_task_group(tg_ac) rt.finish_task_group(tg_b) return Program(dev, rt).resolve_program() diff --git a/iron/operators/gemv/test.py b/iron/operators/gemv/test.py index d4a46c47..e2981c8c 100755 --- a/iron/operators/gemv/test.py +++ b/iron/operators/gemv/test.py @@ -94,6 +94,11 @@ def get_batched_params(): return out +@pytest.mark.metrics( + Latency=r"Latency \(us\): (?P[\d\.]+)", + Bandwidth=r"Effective Bandwidth: (?P[\d\.e\+-]+) GB/s", + Throughput=r"Throughput: (?P[\d\.e\+-]+) GFLOP/s", +) @pytest.mark.parametrize( "M,K,num_aie_columns,tile_size_input,tile_size_output,num_batches", get_batched_params(), @@ -116,7 +121,13 @@ def test_gemv_batched( "vector": golden["B"].flatten(), } output_buffers = {"output": golden["C"].flatten()} - errors, *_ = run_test( + errors, latency_us, bandwidth_gbps = run_test( operator, input_buffers, output_buffers, rel_tol=0.04, abs_tol=1e-3 ) + + print(f"\nLatency: {latency_us:.1f} us") + gflops = (2.0 * M * K * num_batches) / (latency_us * 1e-6) / 1e9 + print(f"Throughput: {gflops:.6e} GFLOP/s") + print(f"Effective Bandwidth: {bandwidth_gbps:.6e} GB/s\n") + assert not errors, f"batched GEMV failed: {errors}" From cffa6a99679bbc75da08ce04d93a3c4024764041 Mon Sep 17 00:00:00 2001 From: Taimuraz Kaitmazov Date: Fri, 10 Jul 2026 21:09:33 +0300 Subject: [PATCH 4/6] gemv: record that the run split is required for cols>1 even on v1.3.4 Verified on the v1.3.4 pin (mlir-aie #2924 + #3036): the contiguous-run linearization only bypasses the 1023 wrap cap when the whole transfer is contiguous (cols==1, bstride==run). For a real cols>1 gather the inner run stays a wrap dim -- [1, num_batches, 4096] fails aiecc 'Size 4096 exceeds [0:1023]'. Correcting the earlier speculative FUTURE note; the split_run stays. --- iron/operators/gemv/design.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/iron/operators/gemv/design.py b/iron/operators/gemv/design.py index 619e30b0..cad80409 100644 --- a/iron/operators/gemv/design.py +++ b/iron/operators/gemv/design.py @@ -185,12 +185,14 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec): # must be even, so split_run only yields an even run_lo and the predicate requires # even strides. # - # FUTURE: this manual split is only needed on the current mlir_aie pin. Once IRON's - # pin moves past Xilinx/mlir-aie #3036 (LinearizeContiguousBDTransfer for the - # iteration dim, on top of #2924 which canonicalizes a contiguous run to linear form - # and bypasses the 1023 cap via the hardware buffer-length register), split_run / - # MAX_WRAP / GRAN_ELEMS can be dropped and the run supplied as one inner dim - # [num_batches, A_run]. The pin is currently frozen at the last pre-#3016 release. + # NOTE: the split cannot be dropped in the cols>1 case, even on a pin with the + # contiguous-run linearization (mlir-aie #2924 + #3036, present in v1.3.4). That + # linearization only bypasses the 1023 wrap cap when the WHOLE transfer is + # contiguous, i.e. bstride==run (cols==1); there [1, num_batches, run] with strides + # [0, bstride, 1] lowers to one linear buffer-length transfer. For cols>1 the batch + # stride leaves a gap, so the inner run stays a genuine wrap dim: verified on v1.3.4 + # that [1, num_batches, 4096] (a cols=8 run) fails aiecc with "Size 4096 exceeds the + # [0:1023] range", while the split below lowers and runs. So the split stays. # FIXME: pull these shim BD bounds from the MLIR-AIE target model rather than # hard-coding them; they live in verifyStridesWraps in # https://github.com/Xilinx/mlir-aie/blob/main/lib/Dialect/AIEX/IR/AIEXDialect.cpp From a7c5887f1d828ff93edf6c093ba9b573438d157f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20R=C3=B6sti?= Date: Mon, 13 Jul 2026 13:35:59 -0600 Subject: [PATCH 5/6] Trim verbose comment --- iron/operators/gemv/design.py | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/iron/operators/gemv/design.py b/iron/operators/gemv/design.py index cad80409..7570e1bf 100644 --- a/iron/operators/gemv/design.py +++ b/iron/operators/gemv/design.py @@ -165,34 +165,15 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec): for col in range(cols) ] - # --- Batch-coalesce (default): one BD per column over all batches. --- - # Replaces the per-batch unroll with a single iterated BD; the stock A_taps/C_taps - # above remain the fallback (and the num_batches==1 path). Access-equivalent to the - # unroll (covered by test_gemv_batched). + # Batch coalescing replaces the per-batch unroll with a single iterated BD. # - # This is NOT a single linear transfer. Within one batch the run is contiguous - # (A_run = (M//cols)*K elements), but the batch stride is the full matrix - # (A_bstride = M*K), so for cols>1 each column gathers its own slice out of every - # batch with a gap in between. Only cols==1 degenerates to bstride==run. So the - # batch dim is a genuine size-uncapped iteration dim and the TAP is required. + # Within one batch the run is contiguous (A_run = (M//cols)*K elements). + # The batch stride is the full matrix (A_bstride = M*K), so for cols>1 each column + # gathers its own slice out of every batch with a gap in between. # # The contiguous run is then split into two wrap dims [run_hi, run_lo] ONLY to fit - # the AIE shim's 10-bit (1023) wrap-size cap. TAP sizes are outermost-first and the - # verifier reverses them, so [1, num_batches, run_hi, run_lo] puts num_batches in the - # size-uncapped dim and the contiguous run in the two capped wrap dims. The shim also - # enforces a 4-byte address granularity on every size and stride (not skipped, even - # for linear transfers); for bf16 (2 bytes) that means run_lo and the batch stride - # must be even, so split_run only yields an even run_lo and the predicate requires - # even strides. + # the AIE shim's 10-bit (1023) wrap-size cap. # - # NOTE: the split cannot be dropped in the cols>1 case, even on a pin with the - # contiguous-run linearization (mlir-aie #2924 + #3036, present in v1.3.4). That - # linearization only bypasses the 1023 wrap cap when the WHOLE transfer is - # contiguous, i.e. bstride==run (cols==1); there [1, num_batches, run] with strides - # [0, bstride, 1] lowers to one linear buffer-length transfer. For cols>1 the batch - # stride leaves a gap, so the inner run stays a genuine wrap dim: verified on v1.3.4 - # that [1, num_batches, 4096] (a cols=8 run) fails aiecc with "Size 4096 exceeds the - # [0:1023] range", while the split below lowers and runs. So the split stays. # FIXME: pull these shim BD bounds from the MLIR-AIE target model rather than # hard-coding them; they live in verifyStridesWraps in # https://github.com/Xilinx/mlir-aie/blob/main/lib/Dialect/AIEX/IR/AIEXDialect.cpp From c4033a51101231c361859a118e3306e318623012 Mon Sep 17 00:00:00 2001 From: andrej Date: Mon, 13 Jul 2026 14:18:15 -0600 Subject: [PATCH 6/6] format --- iron/operators/gemv/design.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iron/operators/gemv/design.py b/iron/operators/gemv/design.py index 7570e1bf..321f7833 100644 --- a/iron/operators/gemv/design.py +++ b/iron/operators/gemv/design.py @@ -168,8 +168,8 @@ def core_body(A_L3L1_fifo, B_L3L1_fifo, C_L1L3_fifo, matvec): # Batch coalescing replaces the per-batch unroll with a single iterated BD. # # Within one batch the run is contiguous (A_run = (M//cols)*K elements). - # The batch stride is the full matrix (A_bstride = M*K), so for cols>1 each column - # gathers its own slice out of every batch with a gap in between. + # The batch stride is the full matrix (A_bstride = M*K), so for cols>1 each column + # gathers its own slice out of every batch with a gap in between. # # The contiguous run is then split into two wrap dims [run_hi, run_lo] ONLY to fit # the AIE shim's 10-bit (1023) wrap-size cap.