Skip to content

Granite 4.2 3B and Granite 4.0-H 1B - #249

Merged
hunhoffe merged 44 commits into
develfrom
granite
Oct 30, 2026
Merged

hunhoffe merged 44 commits into
develfrom
granite

Conversation

@hunhoffe

@hunhoffe hunhoffe commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

This PR contains two models:

  • Granite 4.2 3B (iron/applications/granite/), Llama's dense decoder with Granite's four scalars (attention, residual, embedding and logits multipliers), projections at Precision.HIGH.
  • Granite 4.0-H 1B (iron/applications/granite_hybrid/), the Mamba2 hybrid, built on Granite's classes.

Granite 4.0-H 1B

  • Library pieces it runs: SelectiveScan and state_slices, CausalConv1d/Checkpoints and F.causal_conv1d/ConvState. Their kernels (aie.iron.kernels.ssm) are in the pinned wheel.
  • Dense only. The shipped 1B runs no experts, so its MoE branch (and with it Experts and TopKRouter) stays out; the config refuses a checkpoint with num_local_experts. The MoE toy reference test and its golden go with it.
  • npu1: an empty npu1 profile and a TestEachStep class, as Granite 4.2 3B got in 7c34b69.

A rate fix

generate counted only the tokens through eos over the time a device loop took to draw all of them. Granite 4.0-H 1B draws eos 8th after the benched 13-character prompt, so its TPS read 1.20 where the device draws 6.68. 5c7d8fd counts every token drawn where draws come at once, with a device test of it. Granite 4.2 3B's TPS is unchanged (5.71-5.73).

Measured (Strix Halo, npu2)

Granite 4.2 3B Granite 4.0-H 1B
accuracy, 40 steps (KL mean / max, top-1 swaps) 0.00059 / 0.0018, 0 0.0024 / 0.0063, 2
per-step form, 20 steps 0.0013 / 0.0099, 1 0.0016 / 0.0048, 2
long_context deep decode KL, 4k to whole 0.0010 - 0.0022 0.0007 - 0.0069
decode 5.7 tok/s 6.7 tok/s
tests 15, plus 40 long_context, all pass 15, plus 40 long_context, all pass

Granite 4.0-H 1B's golden gains the per-step check's 20 rows, recorded device-free and teacher-forced on the reference's greedy token.

npu1

Every per-step version of both models compiles for npu1 with no device bound. Granite 4.2 3B's TestEachStep passed on Phoenix before (7c34b69); Granite 4.0-H 1B and both models' long_context deep checks first run on npu1 here, in CI. Each long_context comment says "npu1 still unmeasured" until then.

CI

Each runner needs granite-4.2-3b/ and granite-4.0-h-1b/ under /srv. srv-transfer-granite.zip holds both, with a SHA256SUMS; Granite 4.0-H 1B's golden.npz there replaces the 10-23 copy, which predates the per-step rows.

The nightly grows: long_context's whole-context prompt is about 50 min for Granite 4.2 3B and 12-14 min for Granite 4.0-H 1B on npu2, and longer per step on npu1.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved recurrent-state invalidation can produce incorrect logits, while unchecked selective-scan block sizes can cause execution to hang.

4 open issues
What changed in this PR

Adds Granite 4.2 3B and dense Granite 4.0-H 1B to IRON’s shared language-model framework, including NPU execution, profiles, documentation, and accuracy checks.

Changes:

  • Implements Granite’s scaling and the hybrid model’s Mamba2 layers, supported by selective scan, causal convolution, and checkpointed recurrent state.
  • Corrects generation throughput accounting to include tokens drawn past EOS by device loops.
  • Adds device and reference tests, long-context coverage, and model setup documentation.
File Description
tests/​applications/​long_context/​test.py Adds both Granite models to long-context checks.
tests/​applications/​granite/​test.py Adds Granite device benchmarks and correctness checks.
tests/​applications/​granite/​reference/​test.py Adds toy-model reference comparisons.
tests/​applications/​granite_hybrid/​test.py Adds hybrid device checks and throughput regression coverage.
tests/​applications/​granite_hybrid/​reference/​test.py Adds hybrid reference and configuration checks.
README.md Lists Granite models and command-timeout measurements.
iron/​tests/​common/​test_config.py Includes Granite in shared configuration tests.
iron/​operators/​state_slices.py Adds state-slice memory and core allocation helpers.
iron/​operators/​selective_scan.py Implements Mamba2 selective scan and operator cases.
iron/​operators/​__init__.py Exports SelectiveScan.
iron/​lm/​testing.py Supports recurrent checkpoints in deep-context checks.
iron/​lm/​multimodal.py Delegates batched-draw reporting to the text model.
iron/​lm/​model.py Counts all drawn tokens in generation throughput.
iron/​lm/​causal.py Adds checkpointed states, causal convolution, and draw reporting.
iron/​lm/​__init__.py Exports the new recurrent-state components.
iron/​functional.py Adds causal convolution and its state interface.
iron/​applications/​granite/​README.md Documents checkpoint setup and device testing.
iron/​applications/​granite/​profiles/​npu2.json Supplies Granite NPU2 operator settings.
iron/​applications/​granite/​profiles/​npu1.json Uses default NPU1 operator settings.
iron/​applications/​granite/​model.py Implements Granite configuration and scaling.
iron/​applications/​granite/​__init__.py Exposes Granite through lazy imports.
iron/​applications/​granite_hybrid/​README.md Documents hybrid setup, memory use, and measurements.
iron/​applications/​granite_hybrid/​profiles/​npu2.json Supplies hybrid NPU2 attention settings.
iron/​applications/​granite_hybrid/​profiles/​npu1.json Uses default NPU1 operator settings.
iron/​applications/​granite_hybrid/​model.py Implements dense Mamba2 hybrid layers and configuration.
iron/​applications/​granite_hybrid/​__init__.py Exposes GraniteHybrid through lazy imports.

Review effort: Balanced

Comment thread iron/lm/causal.py Resolved
Comment thread iron/operators/selective_scan.py Resolved
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

CI performance trends

9485f56 (2026-10-29T21:43:33Z)

Krackan - Operators

Operators added: SelectiveScan, generation, pre_push_hook

Operators dropped: visualize

Benchmarks that moved by at least 5% and by more than 2x the spread the runs measured:

Operator Parametrization Metric Previous Current Change
AXPY npu2-AXPY-size_8388608-num_aie_columns_8-tile_size_4096-scalar_factor_3.0 Latency MAD 4.45 11.56 🔴 +159.92%
Clamp npu2-Clamp-size_8388608-num_aie_columns_8-num_channels_2-tile_size_4096-low_-0.75-high_1.25 Latency MAD 5.92 2.59 🟢 -56.21%
Copy npu2-Copy-bench_flat_4mi Latency MAD 5.68 2.81 🟢 -50.41%
DepthwiseConv1d npu2-DepthwiseConv1d-rows_768-channels_1024-num_aie_columns_8 Latency MAD 18.53 2.18 🟢 -88.21%
ElementwiseAdd npu2-ElementwiseAdd-size_8388608-num_aie_columns_8-tile_size_4096 Latency MAD 1.95 10.98 🔴 +461.98%
ElementwiseMul npu2-ElementwiseMul-size_8388608-num_aie_columns_8-tile_size_4096 Latency MAD 2.00 16.94 🔴 +747.76%
Emit npu2-Emit-eight_carried Latency MAD 2.26 3.08 🔴 +36.14%
GELU npu2-GELU-size_8388608-num_aie_columns_8-num_channels_2-tile_size_4096 Latency MAD 13.61 9.44 🟢 -30.66%
GEMM npu2-GEMM-M_2048-K_2048-N_2048-b_col_maj_True Bandwidth 12.89 8.86 🔴 -31.26%
GEMM npu2-GEMM-M_2048-K_2048-N_2048-b_col_maj_True Latency 1952.30 2840.16 🔴 +45.48%
GEMM npu2-GEMM-M_2048-K_2048-N_2048-b_col_maj_True Latency MAD 6.98 15.82 🔴 +126.78%
GEMM npu2-GEMM-M_2048-K_2048-N_2048-b_col_maj_True Throughput 8799.82 6048.90 🔴 -31.26%
GEMV npu2-GEMV-M_1536-K_12288-num_aie_columns_8-tile_size_input_1-tile_size_output_64 Latency MAD 13.71 16.48 🔴 +20.21%
GEMV npu2-GEMV-M_2048-K_8192-num_aie_columns_8-tile_size_input_1-tile_size_output_256 Latency MAD 4.18 16.32 🔴 +290.53%
GEMV npu2-GEMV-M_8192-K_2048-num_aie_columns_8-tile_size_input_4-tile_size_output_1024 Latency MAD 3.53 12.23 🔴 +246.85%
GEMV npu2-GEMV-M_8192-K_2048-num_aie_columns_8-tile_size_input_4-tile_size_output_512-num_channels_2 Latency MAD 4.03 3.39 🟢 -15.92%
LayerNorm npu2-LayerNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096-weighted_True Latency MAD 12.88 8.70 🟢 -32.47%
LayerNorm npu2-LayerNorm-rows_2048-num_aie_columns_8-num_channels_2-tile_size_4096 Latency MAD 2.81 9.38 🔴 +233.84%
Limbs npu2-Limbs-rows_4096-line_320-num_aie_columns_8 Latency MAD 8.07 6.30 🟢 -21.95%
MHA npu2-MHA-num_heads_1-seq_len_16384-num_pipelines_8 Latency MAD 144.15 54.74 🟢 -62.03%
Magnitude npu2-Magnitude-rows_4096-width_640-num_aie_columns_8 Latency MAD 2.97 1.61 🟢 -45.70%
Merge npu2-Merge-rows_2048-placeholders_((258881, 2048), (258880, 2560)) Latency MAD 6.01 2.12 🟢 -64.67%
PatchPositions npu2-PatchPositions-rows_2560-out_height_672-out_width_912 Latency MAD 1.07 2.35 🔴 +119.07%
RMSNorm npu2-RMSNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096-weighted_True Latency MAD 6.11 5.65 🟢 -7.48%
RMSNorm npu2-RMSNorm-rows_2048-num_aie_columns_8-num_channels_2-tile_size_4096 Latency MAD 2.97 1.16 🟢 -60.89%
ReLU npu2-ReLU-size_8388608-num_aie_columns_8-num_channels_2-tile_size_4096 Latency MAD 2.13 5.13 🔴 +140.37%
Resample npu2-Resample-height_3024-width_4032-out_height_672-out_width_912 Latency MAD 21.81 6.95 🟢 -68.12%
ResampleTaps npu2-ResampleTaps-in_size_4032-out_size_912 Latency MAD 0.87 2.05 🔴 +135.49%
RoPE npu2-RoPE-rows_4096-cols_512-angle_rows_8 Latency MAD 1.94 3.02 🔴 +55.48%
RowwiseAdd npu2-RowwiseAdd-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Latency MAD 5.70 8.86 🔴 +55.59%
RowwiseMul npu2-RowwiseMul-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Latency MAD 8.30 7.21 🟢 -13.14%
Sample npu2-Sample-llama Latency MAD 0.77 2.31 🔴 +200.13%
SiLU npu2-SiLU-size_8388608-num_aie_columns_8-tile_size_4096 Latency MAD 7.52 3.35 🟢 -55.46%
Sigmoid npu2-Sigmoid-size_8388608-num_aie_columns_8-num_channels_2-tile_size_4096 Latency MAD 6.17 6.58 🔴 +6.75%
Softmax npu2-Softmax-rows_4096-cols_512-num_aie_columns_2-num_channels_2 Latency MAD 3.06 2.76 🟢 -9.80%
Tanh npu2-Tanh-size_8388608-num_aie_columns_8-num_channels_2-tile_size_4096 Latency MAD 2.29 4.99 🔴 +117.67%
flm.DequantBFP npu2-flm.DequantBFP-K_6144-N_1536 Latency MAD 4.77 4.33 🟢 -9.33%
flm/prefill_attn kind_attn-L_begin_0-L_end_512-max_l_1024-num_kv_heads_1 Bandwidth 5.50 10.31 🟢 +87.46%
flm/prefill_attn kind_attn-L_begin_0-L_end_512-max_l_1024-num_kv_heads_1 Latency 1715.71 915.25 🟢 -46.65%
flm/prefill_attn kind_attn-L_begin_0-L_end_512-max_l_1024-num_kv_heads_1 Throughput 1254.10 2350.92 🟢 +87.46%
Krackan - Applications

Operators added: long_context

Operators dropped: llama, llama/reference

Phoenix - Operators

Operators added: SelectiveScan, pre_push_hook

Operators dropped: visualize

Benchmarks that moved by at least 5% and by more than 2x the spread the runs measured:

Operator Parametrization Metric Previous Current Change
AXPY npu1-AXPY-size_8388608-num_aie_columns_4-tile_size_4096-scalar_factor_3.0 Bandwidth 16.72 31.24 🟢 +86.90%
AXPY npu1-AXPY-size_8388608-num_aie_columns_4-tile_size_4096-scalar_factor_3.0 Latency 3010.99 1610.97 🟢 -46.50%
AXPY npu1-AXPY-size_8388608-num_aie_columns_4-tile_size_4096-scalar_factor_3.0 Latency MAD 1131.82 106.92 🟢 -90.55%
AXPY npu1-AXPY-size_8388608-num_aie_columns_4-tile_size_4096-scalar_factor_3.0 Throughput 5.57 10.41 🟢 +86.90%
Clamp npu1-Clamp-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096-low_-0.75-high_1.25 Bandwidth 24.74 10.03 🔴 -59.45%
Clamp npu1-Clamp-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096-low_-0.75-high_1.25 Latency 1356.22 3344.22 🔴 +146.58%
Clamp npu1-Clamp-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096-low_-0.75-high_1.25 Latency MAD 322.26 344.32 🔴 +6.85%
Clamp npu1-Clamp-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096-low_-0.75-high_1.25 Throughput 6.19 2.51 🔴 -59.45%
Copy npu1-Copy-bench_flat_4mi Bandwidth 5.16 5.42 🟢 +5.07%
Copy npu1-Copy-bench_flat_4mi Latency MAD 147.66 139.21 🟢 -5.72%
ElementwiseAdd npu1-ElementwiseAdd-size_8388608-num_aie_columns_4-tile_size_4096 Bandwidth 32.27 14.87 🔴 -53.93%
ElementwiseAdd npu1-ElementwiseAdd-size_8388608-num_aie_columns_4-tile_size_4096 Latency 1559.66 3385.76 🔴 +117.08%
ElementwiseAdd npu1-ElementwiseAdd-size_8388608-num_aie_columns_4-tile_size_4096 Latency MAD 10.39 620.02 🔴 +5867.77%
ElementwiseAdd npu1-ElementwiseAdd-size_8388608-num_aie_columns_4-tile_size_4096 Throughput 5.38 2.48 🔴 -53.93%
ElementwiseMul npu1-ElementwiseMul-size_8388608-num_aie_columns_4-tile_size_4096 Bandwidth 18.65 15.68 🔴 -15.95%
ElementwiseMul npu1-ElementwiseMul-size_8388608-num_aie_columns_4-tile_size_4096 Latency 2698.80 3210.83 🔴 +18.97%
ElementwiseMul npu1-ElementwiseMul-size_8388608-num_aie_columns_4-tile_size_4096 Latency MAD 1190.66 732.61 🟢 -38.47%
ElementwiseMul npu1-ElementwiseMul-size_8388608-num_aie_columns_4-tile_size_4096 Throughput 3.11 2.61 🔴 -15.95%
Emit npu1-Emit-eight_carried Bandwidth 0.13 0.08 🔴 -35.13%
Emit npu1-Emit-eight_carried Latency 15.80 24.36 🔴 +54.15%
Emit npu1-Emit-eight_carried Latency MAD 3.07 10.34 🔴 +236.63%
GELU npu1-GELU-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Bandwidth 18.07 11.28 🔴 -37.55%
GELU npu1-GELU-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Latency 1857.16 2973.65 🔴 +60.12%
GELU npu1-GELU-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Latency MAD 8.58 689.56 🔴 +7940.60%
GELU npu1-GELU-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Throughput 4.52 2.82 🔴 -37.55%
GEMM npu1-GEMM-M_2048-K_2048-N_2048-b_col_maj_True Latency 7899.84 8312.92 🔴 +5.23%
GEMM npu1-GEMM-M_2048-K_2048-N_2048-b_col_maj_True Latency MAD 248.04 853.29 🔴 +244.01%
LayerNorm npu1-LayerNorm-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096-weighted_True Bandwidth 16.10 9.91 🔴 -38.43%
LayerNorm npu1-LayerNorm-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096-weighted_True Latency 2084.64 3386.06 🔴 +62.43%
LayerNorm npu1-LayerNorm-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096-weighted_True Latency MAD 936.92 375.65 🟢 -59.91%
LayerNorm npu1-LayerNorm-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096-weighted_True Throughput 24.14 14.86 🔴 -38.43%
LayerNorm npu1-LayerNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Bandwidth 11.16 10.15 🔴 -9.02%
LayerNorm npu1-LayerNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Latency 3007.15 3305.32 🔴 +9.92%
LayerNorm npu1-LayerNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Latency MAD 202.86 316.00 🔴 +55.78%
LayerNorm npu1-LayerNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Throughput 16.74 15.23 🔴 -9.02%
Log npu1-Log-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Latency MAD 294.45 147.17 🟢 -50.02%
MHA npu1-MHA-num_heads_1-seq_len_16384-num_pipelines_4 Latency MAD 131.20 79.27 🟢 -39.58%
Merge npu1-Merge-rows_2048-placeholders_((258881, 2048), (258880, 2560)) Bandwidth 0.20 0.18 🔴 -9.54%
Merge npu1-Merge-rows_2048-placeholders_((258881, 2048), (258880, 2560)) Latency 81.79 90.41 🔴 +10.54%
Merge npu1-Merge-rows_2048-placeholders_((258881, 2048), (258880, 2560)) Latency MAD 3.53 13.41 🔴 +280.14%
PatchPositions npu1-PatchPositions-rows_2560-out_height_672-out_width_912 Bandwidth 0.41 0.37 🔴 -10.28%
PatchPositions npu1-PatchPositions-rows_2560-out_height_672-out_width_912 Latency 123.54 137.69 🔴 +11.46%
PatchPositions npu1-PatchPositions-rows_2560-out_height_672-out_width_912 Latency MAD 11.66 40.98 🔴 +251.55%
RMSNorm npu1-RMSNorm-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096-weighted_True Bandwidth 11.72 28.17 🟢 +140.23%
RMSNorm npu1-RMSNorm-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096-weighted_True Latency 2862.69 1191.63 🟢 -58.37%
RMSNorm npu1-RMSNorm-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096-weighted_True Latency MAD 828.02 9.62 🟢 -98.84%
RMSNorm npu1-RMSNorm-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096-weighted_True Throughput 11.72 28.16 🟢 +140.23%
RMSNorm npu1-RMSNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Bandwidth 10.52 9.66 🔴 -8.15%
RMSNorm npu1-RMSNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Latency 3189.93 3472.89 🔴 +8.87%
RMSNorm npu1-RMSNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Latency MAD 299.98 585.71 🔴 +95.25%
RMSNorm npu1-RMSNorm-rows_2048-num_aie_columns_4-num_channels_2-tile_size_4096 Throughput 10.52 9.66 🔴 -8.15%
ReLU npu1-ReLU-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Bandwidth 11.14 15.77 🟢 +41.56%
ReLU npu1-ReLU-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Latency 3012.13 2127.83 🟢 -29.36%
ReLU npu1-ReLU-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Latency MAD 385.35 1093.22 🔴 +183.70%
ReLU npu1-ReLU-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Throughput 2.78 3.94 🟢 +41.56%
Resample npu1-Resample-height_3024-width_4032-out_height_672-out_width_912 Latency MAD 179.90 256.39 🔴 +42.51%
ResampleTaps npu1-ResampleTaps-in_size_4032-out_size_912 Latency MAD 90.97 63.37 🟢 -30.34%
RoPE npu1-RoPE-rows_4096-cols_512-angle_rows_8 Latency MAD 38.49 25.85 🟢 -32.83%
RowwiseAdd npu1-RowwiseAdd-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096 Bandwidth 17.17 28.53 🟢 +66.20%
RowwiseAdd npu1-RowwiseAdd-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096 Latency 1954.89 1176.23 🟢 -39.83%
RowwiseAdd npu1-RowwiseAdd-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096 Latency MAD 880.47 24.91 🟢 -97.17%
RowwiseAdd npu1-RowwiseAdd-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096 Throughput 4.29 7.13 🟢 +66.20%
RowwiseMul npu1-RowwiseMul-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096 Bandwidth 16.04 27.27 🟢 +70.03%
RowwiseMul npu1-RowwiseMul-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096 Latency 2092.42 1230.61 🟢 -41.19%
RowwiseMul npu1-RowwiseMul-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096 Latency MAD 997.75 76.55 🟢 -92.33%
RowwiseMul npu1-RowwiseMul-rows_2048-num_aie_columns_2-num_channels_2-tile_size_4096 Throughput 4.01 6.82 🟢 +70.03%
Sample npu1-Sample-llama Latency MAD 16.63 13.27 🟢 -20.21%
Sigmoid npu1-Sigmoid-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Latency MAD 74.10 9.01 🟢 -87.84%
Softmax npu1-Softmax-rows_4096-cols_512-num_aie_columns_2-num_channels_2 Latency MAD 5.66 11.42 🔴 +101.79%
Tanh npu1-Tanh-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Bandwidth 13.37 29.40 🟢 +119.97%
Tanh npu1-Tanh-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Latency 2510.26 1141.16 🟢 -54.54%
Tanh npu1-Tanh-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Latency MAD 948.68 18.97 🟢 -98.00%
Tanh npu1-Tanh-size_8388608-num_aie_columns_4-num_channels_2-tile_size_4096 Throughput 3.34 7.35 🟢 +119.97%
Phoenix - Applications

Operators added: long_context

Operators dropped: llama/reference

@hunhoffe hunhoffe left a comment

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.

Looks reasonable to me! Follow on work could include performance tuning and double check on operator/library deduplication.

@hunhoffe

Copy link
Copy Markdown
Collaborator Author

@copilot can you please address all review comments from copilot? Be thorough, explain what you did and didn't do.

Copilot AI commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

@copilot can you please address all review comments from copilot? Be thorough, explain what you did and didn't do.

Addressed all four Copilot findings in a8f0362:

  • Recurrent invalidation: Session.forget() now calculates the safe restart boundary using the original processed endpoint, before truncating history. The 100-token → forget(70) → 71-token logits regression rewinds to 64 and matches a fresh run. Tests also cover forgetting at zero, a chunk boundary, and the current endpoint. Checkpoints.restart() itself needed no change.
  • SelectiveScan blocks: compatibility checks reject nonpositive blocks, nonmultiples of SLICE, and block row counts that do not divide rows. Tests cover explicit and automatically resolved column counts, plus valid block controls.
  • CLI: added main(GraniteHybrid) under the module entry-point guard, with a subprocess --help regression.
  • Random weights: the checkpoint-specific early-EOS timing test now skips when random_weights is not None, including seed 0. The trained-checkpoint assertion and TPS calculation are unchanged.

Validation: all 139 relevant shape/shared-LM/Granite reference tests passed; the 21 targeted cases also passed after CodeQL. Temporarily removing the fixes reproduced the invalid-block, non-boundary-forget, and missing-CLI failures. Black, REUSE, secret scanning, and CodeQL passed (zero alerts).

Not done: no performance tuning, broad operator/library deduplication, or dependency changes. No NPU or checkpoint was available here, so real-device EOS timing and throughput were not rerun; random-weight device-test collection was checked, but its hardware execution was not.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

Unresolved issues can cause unnecessary decoding after an initial EOS and graph-recording failures when convolution chunks are smaller than the required history.

No open issues 🎉

2 previously missed issues

In code that hasn't changed since last review

Medium severity Handle chunks smaller than convolution history

iron/​functional.py:157

The staging buffer assumes chunk >= K - 1, but neither CausalConv1d nor Checkpoints.of() enforces that constraint. For a four-tap convolution with a two-row chunk, first is -1 and flat[-1:2] has zero rows, so copying the three-row history fails while recording the graph. Please reserve at least K - 1 history rows independently of the chunk size, or explicitly reject this configuration at construction.

Medium severity Avoid repeated decode steps after first-token EOS

iron/​lm/​model.py:533

When the first token is EOS, device_draws() has not yet entered loop.steps(max_tokens - 1). With at_once true, this condition now runs every remaining decode step even though generate() returns only the initial EOS, potentially delaying completion for a large token budget. Retain the early exit for first-token EOS while counting already-drawn tokens when EOS occurs later.

4 resolved since last review

Review effort: Balanced

@hunhoffe

Copy link
Copy Markdown
Collaborator Author

@copilot please address review findings:
Unresolved issues can cause unnecessary decoding after an initial EOS and graph-recording failures when convolution chunks are smaller than the required history.

No open issues 🎉

2 previously missed issues
In code that hasn't changed since last review

Medium severity Handle chunks smaller than convolution history
iron/​functional.py:157

The staging buffer assumes chunk >= K - 1, but neither CausalConv1d nor Checkpoints.of() enforces that constraint. For a four-tap convolution with a two-row chunk, first is -1 and flat[-1:2] has zero rows, so copying the three-row history fails while recording the graph. Please reserve at least K - 1 history rows independently of the chunk size, or explicitly reject this configuration at construction.

Medium severity Avoid repeated decode steps after first-token EOS
iron/​lm/​model.py:533

When the first token is EOS, device_draws() has not yet entered loop.steps(max_tokens - 1). With at_once true, this condition now runs every remaining decode step even though generate() returns only the initial EOS, potentially delaying completion for a large token budget. Retain the early exit for first-token EOS while counting already-drawn tokens when EOS occurs later.

Copilot AI commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

@copilot please address review findings:
Unresolved issues can cause unnecessary decoding after an initial EOS and graph-recording failures when convolution chunks are smaller than the re...

Addressed both findings in 4abc0df and 56aeb73.

  • Convolution history: chose the explicit-rejection option. CausalConv1d now raises a descriptive ValueError at construction when prefill_chunk < kernel - 1, before graph recording. Eight cases cover invalid chunks (including four taps/two rows), exact-history boundaries, and valid larger chunks. Checkpoints remains unrestricted for other recurrent states. Smaller-than-history convolution chunks are deliberately not supported by this fix.
  • First-token EOS: generation now stops before entering loop.steps() when the initial draw is EOS. Later EOS in batched generation still counts every already-drawn token without returning tokens past EOS. Twelve device-free cases exercise the real device_draws() iterator with a fake loop/clock, checking decode calls, output, TTFT, and TPS for first/later/no EOS, streaming/generation, and one-/six-token budgets.

Validation: both regressions failed before their fixes. All 66 targeted tests passed, including both Granite reference suites. The broader available suite passed 82 tests, with 18 optional-tokenizer skips. Two additional checkpoint-dependent checks failed because this sandbox lacks the Gemma4/EmbeddingGemma2 files; excluding that file produced the green broader run. Black, REUSE, secret scanning, and CodeQL passed (zero alerts).

No dependency changes, performance tuning, or broad deduplication. No real-NPU execution or throughput measurements were performed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The shape checks allow state sizes not divisible by SLICE to reach the vectorized B/C gather, causing out-of-bounds loads and invalid alignment assumptions.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

High severity Require state_size to be a multiple of SLICE

iron/​operators/​selective_scan.py:139

Please also require state_size to be a multiple of SLICE. For example, heads=2, head_dim=16, state_size=24, groups=1 passes both shape checks, but the B/C gather emits a 16-element load at offset 72 into an 82-element combined row; it also declares 32-byte alignment for misaligned C offsets. Reject these sizes here so they cannot reach the vectorized gather with out-of-bounds accesses.

Review effort: Balanced

hunhoffe and others added 6 commits October 29, 2026 12:43
a0162c7 moved eighteen applications out of #222 to their own pull
requests, each with its checkpoints, goldens and the library code only
it exercises. Granite goes first: this ports 46a5d87's model, profiles,
README and device-free tests back in, unchanged, against today's iron.lm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Granite's GraniteConfig covers test_config.py's key-mapping check
alongside LlamaConfig's, and long_context's APPLICATIONS holds it to
the same one-compile, any-context guarantee as Llama and Gemma 4; its
deep_kl figures are 46a5d87's own, measured at 4095 positions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8c25885 gave every model test fixture a model.release() after the
module's (and TestEachStep's) tests, so the next model's CREATE_BO
doesn't meet a full heap behind a reference cycle still awaiting GC.
Granite's fixtures, restored from before that commit, didn't have it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
754e3ae cut iron/lm's and the three #222 applications' docstrings to
what a caller relies on, the rationale left for the commits that made
it. Granite's, restored from before that commit, kept the longer form;
this brings its module and Granite class docstrings to the same register.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
74f7004's table (cut from b1e224c6 to #222's models) left out Granite's
row when it moved out in a0162c7. Restored at its measured figure
(86.5 s on this host today, against b1e224c6's 86.5 s): its deepest
prompt chunk, at its native 131072 context, as one command.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
bd6c053, d23627e and fdaaab1 widened long_context's five context
sizes to the per-step form (an xclbin a step, the host starting each on
the token the last one drew) on npu1 and npu2; Granite's entry, restored
from before that work, was still full-ELF only. Granite has an npu1
profile (empty, every operator's device defaults), so it joins at
each_step=("npu1", "npu2") like Llama's and Gemma 4's.

Both forms measured on this host's npu2, 4096 to the whole 131072
context: decode deep in the cache KL 0.0010 to 0.0022, its deepest prompt
chunk 1.7e-8 to 1.4e-4, top-1 agreeing throughout. npu1 is still
unmeasured on this host.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
hunhoffe and others added 15 commits October 29, 2026 14:25
Profile.make merged the call's keywords over the profile's tunables, so a
tunable passed explicitly as None (mlir-aie's 'not given') overwrote the
profile's value with None, and resolve() then chose its own. Both
Profile.make and the graph recorder, which constructs every step through
it, now drop None keywords first, as Operator construction does.

Co-Authored-By: Claude <noreply@anthropic.com>
GEMV, GEMM, DepthwiseConv1d, Limbs and Magnitude declared cases at 8
columns, which NPU1 (4 columns) refuses, so 15 of its 165 default cases
skipped there: every decode-sized GEMV among them, and GEMV,
DepthwiseConv1d, Limbs and Magnitude had no bench case on Phoenix for
the trends to track. A case of 8 columns now takes all of a narrower
device. On NPU2 every case and label is unchanged, so the benchmark
history keeps its rows.

test_npu1_runs_every_case_of_an_operator_it_runs holds the rule: an
operator NPU1 runs at all resolves every case there. All 15 cases compile
for npu1 on xcoradaie211; none has run on a Phoenix device yet.

Co-Authored-By: Claude <noreply@anthropic.com>
Zero rounds is falsy, so the command line sampled instead of checking;
one round compared nothing and printed a zero denominator. Both now stop
at the parser, before the checkpoint loads.

Co-Authored-By: Claude <noreply@anthropic.com>
It polled for 120 s and then yielded either way, so a server that died or
never came up surfaced later as a refused request. It now raises when the
server exits early or stays silent, and stops a group that outlived it.

Co-Authored-By: Claude <noreply@anthropic.com>
run_test's comment named IRON_RUNTIME, which mlir-aie reads as
NPU_RUNTIME, and its docstring a call_reference that is _call_reference's
reference; Profile.load's Enum comment was garbled; kernel_cycles guarded
on ExternalFunction.recipe_key, which every pinned mlir-aie has; and the
heap-exhaustion test set IRON_XFAIL_DEVICE_HEAP, read by nothing since
169ae38.

Co-Authored-By: Claude <noreply@anthropic.com>
The black check's verdict was grep's, which matched black's own 'Oh no!'
line once the two filters had removed the rest, so the hook passed a
file black refused. It now reads black's exit status.

Co-Authored-By: Claude <noreply@anthropic.com>
Config.to_json asked each field's value whether it had a to_json, for
Llama3RopeScaling's, which existed only to put back the rope_type its
from_dict dropped. rope_type is now a field (always llama3, which
from_dict still checks), so dataclasses.asdict is the exact inverse; a
nested Config still writes its own to_json.

Co-Authored-By: Claude <noreply@anthropic.com>
Golden read IRON_RECORD_GOLDENS at construction, so its tests set and
cleared the variable with monkeypatch. It now takes record=, None
reading the variable as before (pytest --record-goldens is unchanged),
and the tests say which they mean. Two of them ran a model's graph
reference with no device bound, so they failed on a host without an NPU,
which AGENTS.md lists iron/tests/common as needing none; they take the
npu2 fixture. The checkpoint test that needs recording off skips under
--record-goldens instead.

Co-Authored-By: Claude <noreply@anthropic.com>
Each hasattr and getattr-with-a-default now states what reaches it:
Operand re-raises its own property by looking in vars(Operand); Handle's
reflected operators are vars(Handle)'s; a broadcast operand's or a
described argument's shape is read where is_operand says it has one; a
frozen parent is a dataclass's own parameter; an extent's word is an
ExtentWord's; a checkpoint tree's path and file are its _Module's or
_Layers'; a model's repr reads a DecoderConfig's context; the model to
tune is the export whose class defines tune_calls; and a kv kernel's
first entry is read from its own fields. The tests' probes follow.

Co-Authored-By: Claude <noreply@anthropic.com>
Imports inside functions are now at the top of their module unless one
of two reasons holds, which a comment beside it names: a cycle (the
graph and iron.operators, golden and checkpoint, causal and lm.tune,
testing and declare), or an optional dependency (the models extra's
jinja2 and tokenizers, the examples' soundfile, and torch and
transformers where only recording a golden needs them). record.py's
visualize import, golden's tempfile, uuid4, pkgutil and the tests'
support imports had neither. tools.shipped, a one-line wrapper kept
lazy only by that import, is gone: its callers make Shipped themselves.

Co-Authored-By: Claude <noreply@anthropic.com>
EncoderLayer set each of DecoderLayer's fields again by hand. With its
norm a LayerNorm (DecoderLayer.norm) and its MLP and NoCache passed in,
DecoderLayer's constructor makes the same layer, in the same order.

Co-Authored-By: Claude <noreply@anthropic.com>
Each xdist worker and the controller made its own CSVReporter over the
same path, each renaming its rows over the file, and the controller,
which ran no test and so held none, wrote last: pytest -n auto with
--csv-output left an empty file. The reporter is now a plugin the
controller (or a serial session) registers, recording from the reports
every worker sends it (pytest_runtest_logreport carries the
record_property figures and the bench mark); the tests running at once
are a set, each counted a failed check until it ends, as before.

Co-Authored-By: Claude <noreply@anthropic.com>
Its two fifo-type lambdas served one fifo each, its slices lambda made the
same pattern twice per core, now a list made once, and _slices(op) is the
core_slices property. Its default cases pass on npu2 as before.

Co-Authored-By: Claude <noreply@anthropic.com>
The previous commits' isinstance checks lost pyright's narrowing in two
places and named a field its stubs lack in a third: the extent word's
divisor is read under its own isinstance, a nested dataclass instance
(not its class) goes to asdict, and a frozen parent's dataclass
parameters are read through cast(Any, ...).

Co-Authored-By: Claude <noreply@anthropic.com>
Gather and GatherWords declare no Testing (a gather's control words name
device addresses), so no gather had a row in the benchmark history. A
prompt chunk's 2048 ids from EmbeddingGemma 2's table are now checked
against numpy each call and timed end to end, the host's encoding of the
ids included: 434 us on xcoradaie211 (npu2), 4.8 GB/s of rows.

Co-Authored-By: Claude <noreply@anthropic.com>
Recording.dot() had no caller outside its own test, so this drops
iron/common/graph/visualize.py, Recording.dot(), the test, and the
graphviz apt package the CI image installed only for its dot binary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The selective-scan benchmark counts only output elements rather than recurrence and readout arithmetic, understating reported throughput by at least 160×.

1 open issue
2 resolved since last review

Review effort: Balanced

Comment thread iron/operators/selective_scan.py Resolved

@hunhoffe hunhoffe left a comment

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.

Looks decent to me!

hunhoffe and others added 2 commits October 29, 2026 15:29
NPU_RUNTIME gets its explanation in the programming guide's "Getting
Set Up" alongside NPU_CACHE_HOME, with AGENTS.md trimmed to a pointer;
DOCS_GITHUB_REF stays an inline comment at its one call site, matching
the other CI-only vars.

SelectiveScan inherited Operator.ops()'s one-op-per-output-element
count, understating its bench throughput about 160x: the state update
and C readout do five ops per (row, head, head_dim, state_size)
element the output doesn't carry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
mkdocs build --strict only ran on push to devel, so a broken doc link
or anchor surfaced after merge; ci-lint.yml now runs it as a PR check.

docs/models.md hand-duplicates README's models table with GitHub-
absolute links in place of relative ones, since it isn't a symlink of
README the way installation.md is. Each now points at the other so a
new model updates both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@hunhoffe
hunhoffe enabled stacked auto-merge October 29, 2026 22:00
@hunhoffe
hunhoffe added this pull request to the merge queue Oct 29, 2026
Merged via the queue into devel with commit ce440fa Oct 30, 2026
14 of 15 checks passed
@hunhoffe
hunhoffe deleted the granite branch October 30, 2026 00:03
hunhoffe added a commit that referenced this pull request Oct 30, 2026
Brings the Granite models in under the lint and type checks this
branch adds:
- Granite's new docstrings take the one-line summary and imperative
  mood ruff's Google convention asks; the local `I` (Mamba's inner
  width) is `d_inner`.
- `SelectiveScan` joins `iron.operators`'s literal `__all__`, so it is
  catalogued as devel's computed list catalogued it.
- `SelectiveScan.compatible()` names the device it needs, as MHA's
  build does, and its one arith product carries the operator ignore the
  other eudsl arith sites do.
- devel's `scripts/hooks/pre-push` tweak and the test it added go with
  the hook, which the pre-commit config replaces here; the visualize
  module devel removed stays removed.
- devel's Docs job joins `ci-lint.yml` beside this branch's Types job.

ruff, black, REUSE and every pre-commit hook pass over the repository;
pyright reports no errors against the pinned wheel with mlir-aie#3999
laid over it; `mkdocs build --strict` passes; 2252 tests collect, and
the docs, common, lm, Granite reference and SelectiveScan tests pass
on npu2.

Co-Authored-By: Claude <noreply@anthropic.com>
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.

4 participants