Skip to content

Add single-dispatch layer-by-layer multi-head attention#91

Draft
andrej wants to merge 1 commit into
amd:develfrom
andrej:mha-lxl-sd
Draft

Add single-dispatch layer-by-layer multi-head attention#91
andrej wants to merge 1 commit into
amd:develfrom
andrej:mha-lxl-sd

Conversation

@andrej

@andrej andrej commented Apr 6, 2026

Copy link
Copy Markdown
Collaborator

"Naive" alternative implementation for multi-head attention from the currently checked-in data-flow design. This is a simple layer-by-layer implementation, but it uses the single-dispatch mechanism to fuse it all into one MLIR file and save on CPU roundtrips and XRT overheads.

Includes two variants:

  1. "core": Only does the core matmuls and softmax; assumes projected and repeated inputs Q, K, V. This matches the functionality of the checked-in dataflow MHA.
  2. "projected": Performs the Q, K, V projections, applies a RoPE positional embedding and repeats K and V matrices for grouped-query attention. Takes an embedding vector and RoPE angles as input.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we reuse the reference from the existing mha? (Note: does not include RoPE and Q, K, V projections, but some code reuse should be possible.)

@github-actions

github-actions Bot commented Apr 7, 2026

Copy link
Copy Markdown
Contributor
📊 Test Results for Test Example Applications

1d87fe8 (2026_04_07_21_05_39)

IRONCLAD

Tested on 2026_04_07_21_05_39 at commit 1d87fe8.

Test Checks TTFT (mean)TPS (mean)
llama_3.2_1b_prompt_1024_tokens_1 ✅ 5/5 2.13 n/a
llama_3.2_1b_prompt_1024_tokens_40 ✅ 5/5 2.18 4.31
llama_3.2_1b_prompt_13_tokens_1 ✅ 5/5 2.09 n/a
llama_3.2_1b_prompt_13_tokens_40 ✅ 5/5 2.09 4.31
📈 Trends (vs main branch) for Test Example Applications

1d87fe8 (2026_04_07_21_05_39)

IRONCLAD Trends

llama_3.2_1b

Commit/Date Num Tokens (max)Num Tokens (mean)Num Tokens (median)Num Tokens (min)Num Tokens (stddev)TPS (max)TPS (mean)TPS (median)TPS (min)TPS (stddev)TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)Total (max)Total (mean)Total (median)Total (min)Total (stddev)
130b6ea — 2025-12-05 21:33:1240.00 (+0.00%)40.00 (+0.00%)40.00 (+0.00%)40.00 (+0.00%)0.00 (n/a)4.71 (-0.42%)4.64 (-0.09%)4.64 (+0.65%)4.55 (-0.22%)0.05 (-17.66%)4.41 (-0.34%)4.39 (-0.19%)4.38 (-0.33%)4.37 (-0.15%)0.01 (-25.90%)12.96 (-0.00%)12.80 (+0.07%)12.80 (-0.23%)12.67 (+0.44%)0.09 (-21.12%)
0a6c11c — 2025-12-03 23:35:1540.00 (n/a)40.00 (n/a)40.00 (n/a)40.00 (n/a)0.00 (n/a)4.73 (n/a)4.64 (n/a)4.61 (n/a)4.56 (n/a)0.06 (n/a)4.42 (n/a)4.40 (n/a)4.40 (n/a)4.37 (n/a)0.02 (n/a)12.96 (n/a)12.79 (n/a)12.83 (n/a)12.62 (n/a)0.12 (n/a)

llama_3.2_1b_prompt_1024_tokens_1

Commit/Date TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
1d87fe8 — 2026-04-07 21:00:002.15 (+0.09%)2.13 (+0.08%)2.13 (-0.42%)2.12 (+0.62%)0.01 (-31.21%)
912e6bc — 2026-04-07 19:08:432.15 (n/a)2.13 (n/a)2.13 (n/a)2.11 (n/a)0.02 (n/a)

llama_3.2_1b_prompt_1024_tokens_40

Commit/Date TPS (max)TPS (mean)TPS (median)TPS (min)TPS (stddev)TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
1d87fe8 — 2026-04-07 21:00:004.33 (+2.90%)4.31 (+3.44%)4.31 (+3.58%)4.29 (+3.77%)0.01 (-46.93%)2.29 (+0.48%)2.18 (+0.83%)2.15 (+0.80%)2.13 (+0.61%)0.07 (-4.73%)
912e6bc — 2026-04-07 19:08:434.21 (n/a)4.17 (n/a)4.16 (n/a)4.14 (n/a)0.03 (n/a)2.28 (n/a)2.16 (n/a)2.13 (n/a)2.12 (n/a)0.07 (n/a)

llama_3.2_1b_prompt_13_tokens_1

Commit/Date TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
1d87fe8 — 2026-04-07 21:00:002.10 (-0.10%)2.09 (+0.11%)2.09 (+0.19%)2.09 (+0.00%)0.01 (+8.87%)
912e6bc — 2026-04-07 19:08:432.10 (n/a)2.09 (n/a)2.09 (n/a)2.09 (n/a)0.01 (n/a)

llama_3.2_1b_prompt_13_tokens_40

Commit/Date TPS (max)TPS (mean)TPS (median)TPS (min)TPS (stddev)TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
1d87fe8 — 2026-04-07 21:00:004.36 (+4.23%)4.31 (+3.57%)4.30 (+3.44%)4.29 (+3.23%)0.03 (+128.30%)2.09 (-0.38%)2.09 (-0.04%)2.09 (+0.00%)2.08 (+0.44%)0.01 (-34.93%)
912e6bc — 2026-04-07 19:08:434.18 (n/a)4.16 (n/a)4.16 (n/a)4.15 (n/a)0.01 (n/a)2.10 (n/a)2.09 (n/a)2.09 (n/a)2.07 (n/a)0.01 (n/a)

llama_3.2_1b_prompt_2048_tokens_1

Commit/Date Num_Tokens (max)Num_Tokens (mean)Num_Tokens (median)Num_Tokens (min)Num_Tokens (stddev)TPS (max)TPS (mean)TPS (median)TPS (min)TPS (stddev)TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
897d04e — 2026-03-06 22:56:071.00 (+0.00%)1.00 (+0.00%)1.00 (+0.00%)1.00 (+0.00%)0.00 (n/a)0.00 (n/a)0.00 (n/a)0.00 (n/a)0.00 (n/a)0.00 (n/a)2.68 (-1.06%)2.68 (-1.06%)2.68 (-1.06%)2.68 (-1.06%)0.00 (n/a)
84d3478 — 2026-02-17 23:16:231.00 (n/a)1.00 (n/a)1.00 (n/a)1.00 (n/a)0.00 (n/a)0.00 (n/a)0.00 (n/a)0.00 (n/a)0.00 (n/a)0.00 (n/a)2.70 (n/a)2.70 (n/a)2.70 (n/a)2.70 (n/a)0.00 (n/a)

llama_3.2_1b_prompt_2048_tokens_40

Commit/Date Num_Tokens (max)Num_Tokens (mean)Num_Tokens (median)Num_Tokens (min)Num_Tokens (stddev)TPS (max)TPS (mean)TPS (median)TPS (min)TPS (stddev)TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
897d04e — 2026-03-06 22:56:0740.00 (+0.00%)40.00 (+0.00%)40.00 (+0.00%)40.00 (+0.00%)0.00 (n/a)4.00 (-1.72%)4.00 (-1.72%)4.00 (-1.72%)4.00 (-1.72%)0.00 (n/a)2.70 (-0.44%)2.70 (-0.44%)2.70 (-0.44%)2.70 (-0.44%)0.00 (n/a)
84d3478 — 2026-02-17 23:16:2340.00 (n/a)40.00 (n/a)40.00 (n/a)40.00 (n/a)0.00 (n/a)4.07 (n/a)4.07 (n/a)4.07 (n/a)4.07 (n/a)0.00 (n/a)2.71 (n/a)2.71 (n/a)2.71 (n/a)2.71 (n/a)0.00 (n/a)

@github-actions

github-actions Bot commented Apr 15, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

55c43fb (2026_07_09_22_19_28)

IRON - CI Summary

Examples

iron/applications/llama_3.2_1b
Test Krackan Status Krackan Phoenix Status Phoenix
test_llama_3_2_1b[llama_3.2_1b_prompt_1024_tokens_1] - - -
test_llama_3_2_1b[llama_3.2_1b_prompt_1024_tokens_40] - - -
test_llama_3_2_1b[llama_3.2_1b_prompt_13_tokens_1] - - -
test_llama_3_2_1b[llama_3.2_1b_prompt_13_tokens_40] - - -
Krackan - Examples

IRON

Tested on 2026_07_09_22_19_28 at commit 55c43fb.

iron/applications/llama_3.2_1b
TestChecksTTFT (mean)TPS (mean)
test_llama_3_2_1b[llama_3.2_1b_prompt_1024_tokens_1]✅ 5/52.13n/a
test_llama_3_2_1b[llama_3.2_1b_prompt_1024_tokens_40]✅ 5/52.157.40
test_llama_3_2_1b[llama_3.2_1b_prompt_13_tokens_1]✅ 5/52.09n/a
test_llama_3_2_1b[llama_3.2_1b_prompt_13_tokens_40]✅ 5/52.107.22

Trends:

IRON Trends

iron/applications/llama_3.2_1b

test_llama_3_2_1b[llama_3.2_1b_prompt_1024_tokens_1]

Commit/Date TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
55c43fb — 2026-07-09 22:14:252.14 (+0.00%)2.13 (-0.06%)2.13 (+0.05%)2.10 (-0.52%)0.01 (+43.82%)
c9bc036 — 2026-07-09 21:58:262.14 (n/a)2.13 (n/a)2.13 (n/a)2.11 (n/a)0.01 (n/a)

test_llama_3_2_1b[llama_3.2_1b_prompt_1024_tokens_40]

Commit/Date TPS (max)TPS (mean)TPS (median)TPS (min)TPS (stddev)TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
55c43fb — 2026-07-09 22:14:257.42 (-0.05%)7.40 (-0.06%)7.41 (-0.12%)7.38 (+0.00%)0.02 (-13.27%)2.27 (+0.89%)2.15 (-0.30%)2.12 (-0.61%)2.11 (-0.33%)0.07 (+25.57%)
c9bc036 — 2026-07-09 21:58:267.42 (n/a)7.41 (n/a)7.42 (n/a)7.38 (n/a)0.02 (n/a)2.25 (n/a)2.15 (n/a)2.13 (n/a)2.11 (n/a)0.05 (n/a)

test_llama_3_2_1b[llama_3.2_1b_prompt_13_tokens_1]

Commit/Date TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
55c43fb — 2026-07-09 22:14:252.12 (+1.00%)2.09 (+0.65%)2.10 (+1.30%)2.06 (-0.15%)0.02 (+72.48%)
c9bc036 — 2026-07-09 21:58:262.09 (n/a)2.08 (n/a)2.08 (n/a)2.06 (n/a)0.01 (n/a)

test_llama_3_2_1b[llama_3.2_1b_prompt_13_tokens_40]

Commit/Date TPS (max)TPS (mean)TPS (median)TPS (min)TPS (stddev)TTFT (max)TTFT (mean)TTFT (median)TTFT (min)TTFT (stddev)
55c43fb — 2026-07-09 22:14:257.25 (-0.37%)7.22 (-0.22%)7.23 (-0.11%)7.20 (-0.39%)0.02 (+3.51%)2.11 (+0.48%)2.10 (+0.35%)2.11 (+0.33%)2.07 (+0.44%)0.02 (+3.52%)
c9bc036 — 2026-07-09 21:58:267.28 (n/a)7.24 (n/a)7.24 (n/a)7.22 (n/a)0.02 (n/a)2.10 (n/a)2.09 (n/a)2.10 (n/a)2.06 (n/a)0.02 (n/a)
Phoenix - Examples

IRON

Tested on 2026_07_09_22_13_01 at commit 55c43fb.

Trends:

IRON Trends

Comment thread aie_kernels/aie2/softmax.cc Outdated
Comment on lines +68 to +74
// stats[0] = running max
// stats[1] = running sum (of exp(x - max))
// ---------------------------------------------------------------------------

void softmax_partial_stats_impl(bfloat16 *restrict input,
bfloat16 *stats,
const int32_t vector_size)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For readability, can we make stats a struct rather than an array with hard-coded indices? Also pointer should never alias with the input so we can annotate with *restrict.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have struct softmax_stats *stats as the function argument but if that's hard to do from the design.py it's fine if it doesn't match from there and is just a void * there in the function signature

return;
}

// ---------------------------------------------------------------------------

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure code comment style aligns with rest of the file, not sure these // --- are used elsewhere

Comment on lines +163 to +172
// ---------------------------------------------------------------------------
// Online (partial / tiled) softmax helpers
//
// These three kernels implement a two-pass online softmax that processes a row
// in sub-tile chunks, keeping running max and sum statistics in a small local
// buffer (`stats`). Layout of the stats buffer (bfloat16[16], only [0..1]
// used):
// stats[0] = running max (scaled by log2e)
// stats[1] = running sum (of exp2(x*log2e - max))
// ---------------------------------------------------------------------------

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apply same comments as in aie_kernels/aie2/softmax.cc: comment style, struct instead of array for stats

Comment on lines +189 to +226
// --- Phase 1: find local max (scaled by log2e) -------------------------
float local_max = -INFINITY;
auto it_in1 = aie::cbegin_restrict_vector<SM_VEC_LEN>((bfloat16 *)input);
for (int i = 0; i < elem_iters; i++) {
input_bf16 = *it_in1++;
scaled_accum = aie::mul(input_bf16, log2e_vec);
float chunk_max = aie::reduce_max(scaled_accum.to_vector<bfloat16>());
if (chunk_max > local_max) {
local_max = chunk_max;
}
}

// --- Phase 2: update running max, rescale running sum ------------------
float old_max = (float)stats[0];
float old_sum = (float)stats[1];

if (local_max > old_max) {
// New max is larger — rescale the old sum by exp2(old_max - new_max)
aie::vector<float, SM_VEC_LEN> diff_vec =
aie::broadcast<float, SM_VEC_LEN>(old_max - local_max);
aie::vector<bfloat16, SM_VEC_LEN> corr = aie::exp2<bfloat16>(diff_vec);
old_sum = old_sum * (float)corr[0];
old_max = local_max;
}

// --- Phase 3: accumulate exp2(input * log2e - max) for this chunk ------
aie::vector<bfloat16, SM_VEC_LEN> max_val_vec =
aie::broadcast<bfloat16, SM_VEC_LEN>((bfloat16)old_max);

auto it_in2 = aie::cbegin_restrict_vector<SM_VEC_LEN>((bfloat16 *)input);
for (int i = 0; i < elem_iters; i++) {
input_bf16 = *it_in2++;
scaled_accum = aie::mul(input_bf16, log2e_vec);
exp_in_accum = aie::sub(scaled_accum, max_val_vec);
aie::vector<bfloat16, SM_VEC_LEN> exp_val =
aie::exp2<bfloat16>(exp_in_accum.to_vector<float>());
exp_val_accum = add(exp_val_accum, exp_val);
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make this use the same on-line approach as in aie_kernels/aie2/softmax.cc

Comment on lines 84 to 90
void partial_softmax_alias_bf16(bfloat16 *restrict input_vector,
bfloat16 *restrict output_vector,
bfloat16 *restrict scale_buffer,
const int32_t vector_size,
const int32_t row_idx,
const int32_t num_rows,
const bfloat16 scale)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There appears to be a lot of code duplicated in the new partial softmax functions below. Factor out shared functionality into a separate function used across both, forcing inlining so it does not affect performance -- or, if sharing code is not easily possible, explain what the differences between the two implementations are.

Comment on lines +82 to +87
// The tile is interpreted as a `vector_size`-wide horizontal strip of the
// per-head (S, S) attention-score block; idx[0] is the strip's starting
// column within that block, idx[1] is the strip's row within the block.
// The kernel implements the causal mask in-place by writing `a` to elements
// strictly above the diagonal and copying y -> z everywhere else. This
// avoids materialising an H*S*S mask buffer entirely.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is a general 'triangular fill', not specific to attention. Make the comments reflect this. Consider factoring this out into a separate operator since it has less to do with AXPY, or making it more AXPY-like by allowing a scale or constant addition be applied within the triangular masked region (again using C++ templates).

Comment thread iron/common/fusion.py Outdated
Comment on lines +26 to +34
"""Operator that fuses multiple MLIROperators into one.

Args:
dispatch: Dispatch strategy for the fused operator.
``"auto"`` (default) selects ``"fused"`` on NPU2 and
``"separate"`` on NPU1. ``"fused"`` uses a single-ELF
dispatch (requires NPU2). ``"separate"`` compiles each
sub-operator to its own xclbin and invokes them sequentially.
"""

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fusion's a bit of a big term for what this does. It still reconfigures the array between each operator. Let's say it fuses multiple operators into a "single dispatch." (except for NPU1 where full ELF is not yet supported where this still degenerates into multiple dispatches). The sequential feature would be used on NPU2 for debugging and benchmarking the impact of single-dispatch.

Comment thread iron/common/fusion.py Outdated
Comment on lines -55 to -57
for obj in objs:
obj.filename = f"op{idx}_{obj.filename}"
obj.prefix_symbols = f"op{idx}_"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed? Disambiguating function names between the sub-operators seems desirable. An explanation / reasoning for removing it is a valid response to this if there are good reasons to remove this.

Comment thread iron/common/fusion.py Outdated
Comment on lines -85 to -86
if len(op.get_kernel_artifacts()) > 0:
mlir_artifact.generator.kwargs["func_prefix"] = f"op{idx}_"

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed? Disambiguating function names between the sub-operators seems desirable. An explanation / reasoning for removing it is a valid response to this if there are good reasons to remove this.

Comment thread iron/common/fusion.py Outdated
Comment on lines +275 to +277
Mirrors the pattern from ``chain_swiglu_artifacts`` in
``iron/operators/swiglu_base.py``: each unique operator gets its own
xclbin + insts compiled separately, linked via ``--xclbin-input``.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

irrelevant remove

…lxl)

Add a layer-by-layer (LxL) prefill MHA operator built as an
OperatorSequence, and rename the existing data-flow (DF) MHA operator
for clarity:

- Rename iron/operators/mha -> iron/operators/mha_prefill_df (DF =
  data-flow: single fused kernel).
- Add iron/operators/mha_prefill_lxl (LxL = layer-by-layer: chained
  sub-operators dispatched back-to-back).
- Extend axpy with scale / scalar_add / causal-mask modes and softmax
  with partial/online softmax, used by the LxL design.
- Register the benchmark marker in an operator-local conftest.py.

Update README and operator registrations to the new names.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant