Skip to content

perf(cpu): vectorize the GDN depthwise causal convolution on AArch64 - #693

Open
Aharrypotter wants to merge 4 commits into
UbiquitousLearning:mainfrom
Aharrypotter:perf/gdn-h1a-pr
Open

perf(cpu): vectorize the GDN depthwise causal convolution on AArch64#693
Aharrypotter wants to merge 4 commits into
UbiquitousLearning:mainfrom
Aharrypotter:perf/gdn-h1a-pr

Conversation

@Aharrypotter

@Aharrypotter Aharrypotter commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an AArch64 NEON fast path for the GDN depthwise causal convolution (the Qwen3.5 prefill hot spot) plus the 4B benchmark tooling used to measure it. The vectorized kernel is bitwise identical to the scalar path per toolchain; the focused oracle asserts that rather than assuming it.

Review map

Area Main files What to review
GDN kernel mllm/backends/cpu/kernels/common/gdn/gated_delta_net.cpp NEON 4-channel path (vld3/vld4/vfma/vst3), accumulation order vs scalar body, scalar fallback for non-AArch64 / kernel_size != 4 / channel tails
Kernel oracle tests/cpu/Qwen35GDNConvTest.cpp independent scalar reference, geometry coverage (batch/sequence/channels, production 6144/8192 + tails), chunking and reset semantics, bitwise output and history
Generation contract mllm/models/ARGeneration.{hpp,cpp} + tests/core/ARGenerationTest.cpp min_new_tokens: bounds, EOS suppression, delayed termination, invalid-argument paths across iterator/batch/streaming
Benchmark harness examples/qwen3_5/benchmark_harness.hpp, examples/qwen3_5/main.cpp prefill/TTFT/decode timing, JSONL records, warmup/sample controls, device telemetry (record-only)

Suggested review order: GDN kernel → kernel oracle → generation contract → benchmark harness.

Supported contract

Surface This PR
Fast path AArch64 NEON, kernel_size == 4 (the production Qwen3.5 setting), 4 channels per iteration
Exactness Bitwise-identical output and history vs the scalar body, per toolchain (oracle-asserted, not assumed)
Fallback Scalar path preserved for non-AArch64, kernel_size != 4, and channel tails; no behavior change elsewhere
Public API Kernel signature, tensor layouts, and traversal order unchanged
Generation New optional ARGenerationArgs key min_new_tokens (default 0 = legacy behavior)

Validation

Gate Status Evidence
Kernel oracle (macOS arm64, NEON fast path) PASS Qwen35GDNConvTest 6/6; 1,397 bitwise output+history checks, 0 failures
Device bitwise identity PASS Pixel 9 Pro XL / Tensor G4, all seven oracle layers, NDK r28b build
Android cross-build PASS NDK r27c, arm64-v8a: all three test targets compile (Mllm-Test-Qwen35-GDN-Conv, Mllm-Test-Core-ARGeneration, Mllm-Test-Core-Qwen35BenchmarkHarness)
Static checks PASS git diff --check clean; formatting per .clang-format

Performance evidence

Conclusion: on-device prefill is faster by +30–35% (0.8B) and +18–36% (4B); decode is unchanged within run-to-run noise.

Setup — OnePlus 13T (PKX110, SM8750, Android 16, arm64-v8a), 8 CPU threads, 3 runs per configuration with 5 s cooldown between runs, median reported. Prefill speed in tokens/s, higher is better. Baseline = origin/main @ 2c889d90 (scalar convolution); candidate = this PR's vectorized convolution. Same device, same model artifact, same binary set, measured back-to-back.

Qwen3.5-4B — real-model prefill:

Prompt length Baseline This PR Speedup
64 24.3 t/s 28.6 t/s +18%
128 24.2 t/s 31.9 t/s +32%
256 23.0 t/s 31.2 t/s +36%

Qwen3.5-0.8B — real-model prefill:

Prompt length Baseline This PR Speedup
64 121.4 t/s 163.6 t/s +35%
128 125.3 t/s 163.8 t/s +31%
256 131.2 t/s 171.0 t/s +30%

Decode is unaffected — this PR only touches the prefill convolution path. Decode speed changed by −6%…+2% across all six configurations, i.e. within noise and with no systematic direction.

Operator-level (supplementary) — macOS arm64, convolution only, C=8192, S=517: +69–84% median. The end-to-end numbers above are lower because they include attention and MLP, which this PR does not touch.

⚠️ Methodology note: the "This PR" numbers above were measured with a binary built from a branch that also carried a sequence-aware I8MM projection candidate (see Scope notes). That candidate was independently measured on the same device and rejected — PP69 prefill +4.30% regression vs the ≤2% gate, PP517 halves +3.7%/+2.3% vs the ≥10% line — so it contributes no speedup. The numbers above therefore reflect the vectorized convolution alone, and the I8MM candidate is deliberately not included in this PR.

Files changed

10 files, +897/−37:

  • mllm/backends/cpu/kernels/common/gdn/gated_delta_net.cpp (the vectorized kernel, +39/−1)
  • tests/cpu/Qwen35GDNConvTest.cpp (new oracle, 265 lines) + tests/cpu/CMakeLists.txt
  • examples/qwen3_5/benchmark_harness.hpp, examples/qwen3_5/main.cpp
  • mllm/models/ARGeneration.{hpp,cpp}, tests/core/ARGenerationTest.cpp, tests/core/CMakeLists.txt

Known limits

  • Fast path is AArch64 + kernel_size == 4 only; all other targets/shapes use the incumbent scalar path.
  • End-to-end performance is a single-device (OnePlus 13T) real-model measurement, not an official benchmark; operator-level evidence on host is provided as context.
  • Decode shows no gain (expected — the optimization targets prefill).
  • 4B benchmark harness is the measurement tool for this optimization; it does not change model semantics.

Scope notes

  • Out of scope and not in this PR: the sequence-aware I8MM projection candidate (rejected on device performance grounds, see above), generic Linear dispatcher changes, recurrence/quantization/tokenizer changes.
  • CodeRabbit review findings addressed: generate/streamGenerate invalid-bound tests, --benchmark_warmup benchmark-mode detection, direct includes in tests, GTEST_SKIP on non-Apple platforms, cpu0/online documentation, and the min_new_tokens public-API contract docs. The suggested CTest registration (gtest_discover_tests) was not applied: the repo registers no test target with CTest, and gtest_discover_tests executes the test binary at build time, which cannot run an Android ARM64 binary on the x86 CI runner (build-android failure).
  • No GitHub write occurred during development of the sealed evidence; this PR is the first upstream-facing submission.

Summary by CodeRabbit

  • New Features

    • Added min_new_tokens support across standard, streaming, and chat generation, preventing early end-of-sequence termination.
    • Added Qwen 3.5 benchmark mode with warmups, fixed-length samples, performance metrics, telemetry, and JSONL output.
    • Added validation for benchmark configuration, prompts, token limits, identities, and telemetry requirements.
  • Performance

    • Improved causal convolution performance on supported ARM processors.
  • Tests

    • Added coverage for minimum-token generation, benchmark telemetry validation, and causal convolution correctness.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds Qwen3.5 benchmark execution and telemetry validation, minimum-token controls across generation APIs, and an AArch64 NEON optimization for four-tap GDN convolution. It also adds focused GoogleTest coverage and CMake targets.

Changes

Qwen3.5 benchmark tooling

Layer / File(s) Summary
Telemetry capture and validation
examples/qwen3_5/benchmark_harness.hpp, tests/core/Qwen35BenchmarkHarnessTest.cpp, tests/core/CMakeLists.txt
The harness captures CPU and thermal telemetry, validates required fields and snapshot stability, and tests supported and unsupported platform behavior.
Benchmark runner integration
examples/qwen3_5/main.cpp
The example parses benchmark options, configures the engine, runs warmups and fixed-length samples, validates results, writes JSONL records, and preserves interactive generation.

Minimum-token generation

Layer / File(s) Summary
Minimum-token generation control
mllm/models/ARGeneration.hpp, mllm/models/ARGeneration.cpp
Iterator, batch, and streaming generation validate min_new_tokens, suppress EOS logits before the minimum, and delay EOS termination.
Generation behavior tests
tests/core/ARGenerationTest.cpp
Tests cover chat, batch, and streaming behavior, additional EOS tokens, invalid bounds, and the maximum-length boundary.

AArch64 GDN convolution

Layer / File(s) Summary
NEON convolution path
mllm/backends/cpu/kernels/common/gdn/gated_delta_net.cpp
The four-tap AArch64 path processes four channels with NEON operations and uses scalar handling for remaining channels and other builds.
Convolution correctness tests
tests/cpu/Qwen35GDNConvTest.cpp, tests/cpu/CMakeLists.txt
Tests compare production output and history with a scalar reference across geometry, chunking, state reset, channel tails, and invalid inputs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant runner as qwen3_5/main.cpp
  participant engine as mllm engine
  participant harness as benchmark_harness.hpp
  participant jsonl as JSONL output
  runner->>engine: reset and generate benchmark request
  runner->>harness: capture telemetry
  harness-->>runner: telemetry snapshot
  runner->>harness: validate required and stable telemetry
  runner->>jsonl: write measured result
Loading

Possibly related PRs

Suggested reviewers: yirongjie, chenghuawang, oreomaker

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely identifies the primary change: AArch64 CPU vectorization of the GDN depthwise causal convolution.
Description check ✅ Passed The description is complete and structured, covering the changes, scope, validation, performance evidence, known limits, and review guidance.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 6

🧹 Nitpick comments (9)
mllm/models/ARGeneration.hpp (1)

83-83: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the min_new_tokens argument contract.

min_new_tokens is a caller-visible ARGenerationArgs key. Document its bounds, EOS behavior, and std::invalid_argument conditions at the public generate, streamGenerate, and chat declarations.

As per coding guidelines, “Ensure public APIs, classes, and functions have clear docstrings or comments explaining purpose, parameters, returns, and errors.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mllm/models/ARGeneration.hpp` at line 83, Document the min_new_tokens
contract in the public generate, streamGenerate, and chat declarations: state
its valid bounds, how it affects EOS handling, and when std::invalid_argument is
thrown. Ensure the documentation clearly identifies min_new_tokens as an
ARGenerationArgs key without changing implementation behavior.

Source: Coding guidelines

tests/core/ARGenerationTest.cpp (1)

209-229: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test invalid bounds in batch and streaming generation.

These tests only exercise ARGenerationChatIterator. Add generate and streamGenerate cases for min_new_tokens > max_length, negative min_new_tokens, and non-positive max_length. Both methods have independent validation code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/core/ARGenerationTest.cpp` around lines 209 - 229, The current
invalid-bound tests cover only ARGenerationChatIterator; extend the test
coverage for the generate and streamGenerate APIs, including min_new_tokens
greater than max_length, negative min_new_tokens, and non-positive max_length.
Add cases using the existing test fixtures/helpers and assert each API rejects
these inputs, preserving the independent validation paths in generate and
streamGenerate.
tools/gdn-h1a-4b-r4/test_harness.py (2)

222-222: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use unpacking instead of list concatenation.

Ruff reports RUF005 on this line.

♻️ Proposed fix
-        path.write_text("\n".join(lines + [lines[-1]]) + "\n", encoding="utf-8")
+        path.write_text("\n".join([*lines, lines[-1]]) + "\n", encoding="utf-8")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/gdn-h1a-4b-r4/test_harness.py` at line 222, Update the write_text
construction in the test harness to use iterable unpacking when appending the
final line, replacing the list concatenation that triggers Ruff RUF005 while
preserving the existing output and encoding.

Source: Linters/SAST tools


156-159: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tests for the remaining dispositions and the controller validators.

The suite covers PROMOTE_H1A, REJECT_CORRECTNESS, HARNESS_REHEARSAL_VALID, and many ContractError paths. Two disposition branches stay untested: SCREENING_BLOCKED (mixed ceiling regimes or an unstable p95_over_median) and RETAIN_INCUMBENT (a candidate that does not improve). Both drive promotion decisions. controller.parse_prompt and controller.validate_args are also untested, so the prompt-format and frozen-contract rules have no coverage.

Suggested cases:

  • A candidate with prefill_duration_us equal to the baseline yields RETAIN_INCUMBENT.
  • A record with a differing telemetry_before.ceiling_vector across positions yields SCREENING_BLOCKED.
  • controller.parse_prompt("bad-format") raises argparse.ArgumentTypeError.
  • controller.validate_args rejects threads != 8.

Do you want me to generate these tests?

Also applies to: 241-251

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/gdn-h1a-4b-r4/test_harness.py` around lines 156 - 159, Add tests in
test_harness.py covering the untested analyzer dispositions: verify equal
candidate and baseline prefill duration returns RETAIN_INCUMBENT, and differing
telemetry_before.ceiling_vector values across positions return
SCREENING_BLOCKED. Add controller validator tests confirming
parse_prompt("bad-format") raises argparse.ArgumentTypeError and validate_args
rejects threads values other than 8, using the existing fixture and assertion
patterns.
examples/qwen3_5/benchmark_harness.hpp (1)

78-95: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the cpu0 online assumption.

Line 81 reports online = 1 for CPU 0 without reading sysfs. Most kernels do not expose /sys/devices/system/cpu/cpu0/online because CPU 0 cannot be offlined. The hard-coded value is therefore correct in practice, but the reason is not visible. Add a short comment so the special case is not read as a validation bypass.

♻️ Proposed comment
   for (const int cpu : affinity) {
     const auto base = std::filesystem::path("/sys/devices/system/cpu") / ("cpu" + std::to_string(cpu));
     const auto cpufreq = base / "cpufreq";
+    // Most kernels omit cpu0/online because CPU 0 cannot be offlined, so report it as online.
     const auto online = cpu == 0 ? std::optional<int64_t>(1) : readIntegerFile(base / "online");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/qwen3_5/benchmark_harness.hpp` around lines 78 - 95, Add a short
explanatory comment immediately before the `online` initialization in the CPU
snapshot loop, documenting that CPU 0 cannot be offlined and therefore commonly
lacks a sysfs `online` file; preserve the existing `cpu == 0` special case and
all other behavior.
tests/core/Qwen35BenchmarkHarnessTest.cpp (2)

76-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The test passes without assertions on non-Apple platforms.

The whole body is inside #if defined(__APPLE__). On Linux and Android CI the test reports success and checks nothing. Use GTEST_SKIP() so the report shows that the case did not run.

♻️ Proposed fix
 TEST(Qwen35BenchmarkHarnessTest, MacCaptureFailsClosedWhenTelemetryIsRequired) {
 `#if` defined(__APPLE__)
   const auto errors =
       mllm::examples::qwen3_5::benchmark::validateRequiredTelemetry(mllm::examples::qwen3_5::benchmark::captureTelemetry());
   EXPECT_NE(std::find(errors.begin(), errors.end(), "unsupported_telemetry_platform"), errors.end());
+#else
+  GTEST_SKIP() << "macOS-only telemetry rejection check";
 `#endif`
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/core/Qwen35BenchmarkHarnessTest.cpp` around lines 76 - 82, Update
Qwen35BenchmarkHarnessTest.MacCaptureFailsClosedWhenTelemetryIsRequired so
non-Apple builds call GTEST_SKIP() instead of completing without assertions;
keep the existing telemetry validation assertion unchanged under __APPLE__.

4-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Include <algorithm> for std::find.

Lines 60 and 80 call std::find. The file includes only <gtest/gtest.h> and benchmark_harness.hpp. benchmark_harness.hpp includes <algorithm>, so the build succeeds today, but the dependency is indirect. Include the header directly.

♻️ Proposed fix
 `#include` <gtest/gtest.h>
 
+#include <algorithm>
+
 `#include` "benchmark_harness.hpp"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/core/Qwen35BenchmarkHarnessTest.cpp` around lines 4 - 6, Update
Qwen35BenchmarkHarnessTest.cpp to include the standard <algorithm> header
directly, alongside its existing includes, because the test uses std::find; do
not rely on benchmark_harness.hpp’s transitive include.
tools/gdn-h1a-4b-r4/controller.py (2)

83-84: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

--cpuset and --threads are options that only one value passes.

Line 102 rejects every value other than 8 and "0-7", which are also the defaults. The options therefore cannot change behaviour. Either remove them and use module constants, or keep them and state in the help text that the r4 contract freezes both values.

♻️ Proposed refactor
-    parser.add_argument("--cpuset", default="0-7")
-    parser.add_argument("--threads", type=int, default=8)
+    parser.add_argument("--cpuset", default=REQUIRED_CPUSET, help=f"frozen by the r4 contract to {REQUIRED_CPUSET}")
+    parser.add_argument("--threads", type=int, default=REQUIRED_THREADS,
+                        help=f"frozen by the r4 contract to {REQUIRED_THREADS}")

Also applies to: 102-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/gdn-h1a-4b-r4/controller.py` around lines 83 - 84, Update the argument
parsing and validation around --cpuset and --threads so these options can either
meaningfully configure behavior or are removed in favor of module constants. If
retaining them, remove the unconditional rejection of values other than "0-7"
and 8, and document in their help text that the r4 contract intentionally
freezes those values.

150-151: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Hard-coded token counts in controller.py and analyze.py. Both programs repeat the literals 64 and 63 even though the run contract already records generated_tokens and decode_steps. The producer and the consumer can therefore drift apart, and the analyzer would reject every position with "wrong token denominator" after a contract change.

  • tools/gdn-h1a-4b-r4/controller.py#L150-L151: pass str(contract["generated_tokens"]) to --max_new_tokens, and derive decode_steps from the same constant at Lines 238-239.
  • tools/gdn-h1a-4b-r4/analyze.py#L139-L146: compare stats["generated_tokens"] and stats["decode_steps"] against contract["generated_tokens"] and contract["decode_steps"], and divide decode_duration_us by contract["decode_steps"].
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/gdn-h1a-4b-r4/controller.py` around lines 150 - 151, The token-count
contract is duplicated as hard-coded 64/63 values across the producer and
analyzer. In tools/gdn-h1a-4b-r4/controller.py lines 150-151, pass
contract["generated_tokens"] to --max_new_tokens and derive decode_steps from
that same contract value at lines 238-239; in tools/gdn-h1a-4b-r4/analyze.py
lines 139-146, validate stats["generated_tokens"] and stats["decode_steps"]
against the corresponding contract fields and divide decode_duration_us by
contract["decode_steps"].
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/qwen3_5/main.cpp`:
- Around line 100-104: Update the benchmark_jsonl validation around jsonl_path
to use error_code-aware filesystem checks, inspect size_error immediately after
file_size, and distinguish filesystem failures from the non-empty-file case.
Preserve the invalid_argument rejection only when the path exists and has
nonzero size, while reporting filesystem errors through the existing
error-handling approach.
- Around line 81-83: Update the benchmark_mode detection expression in main to
include benchmark_warmup.isSet(), ensuring that using only --benchmark_warmup
enters benchmark mode and triggers the existing required-option validation.

In `@mllm/models/ARGeneration.cpp`:
- Around line 35-41: Validate temperature in all three generation entry points
before sampling, rejecting non-finite or non-positive values so invalid inputs
cannot reach sampleTemperature; update the relevant validation paths around the
existing max_length/min_new_tokens checks. Add a regression test covering
min_new_tokens greater than 1 with temperature set to -1.0F.

In `@tests/core/CMakeLists.txt`:
- Around line 9-12: Register Mllm-Test-Core-Qwen35BenchmarkHarness with CTest
using the same gtest_discover_tests pattern already used by the other GTest core
targets. Add the discovery call after the target setup so the new executable is
included in the test framework.

In `@tests/cpu/Qwen35GDNConvTest.cpp`:
- Around line 13-17: Update Qwen35GDNConvTest.cpp to include the standard
headers <algorithm> and <stdexcept> directly, covering its uses of std::fill and
std::invalid_argument rather than relying on transitive includes.

In `@tools/gdn-h1a-4b-r4/analyze.py`:
- Around line 268-274: Update the exception handling in main around the
analyze(args.result_root) call to catch general Exception in addition to
ContractError, converting any unexpected analysis failure into the same
HARNESS_REJECTED result with the exception message in errors. Preserve the
existing output-file writing, disposition printing, and return-code logic for
both handled error types.

---

Nitpick comments:
In `@examples/qwen3_5/benchmark_harness.hpp`:
- Around line 78-95: Add a short explanatory comment immediately before the
`online` initialization in the CPU snapshot loop, documenting that CPU 0 cannot
be offlined and therefore commonly lacks a sysfs `online` file; preserve the
existing `cpu == 0` special case and all other behavior.

In `@mllm/models/ARGeneration.hpp`:
- Line 83: Document the min_new_tokens contract in the public generate,
streamGenerate, and chat declarations: state its valid bounds, how it affects
EOS handling, and when std::invalid_argument is thrown. Ensure the documentation
clearly identifies min_new_tokens as an ARGenerationArgs key without changing
implementation behavior.

In `@tests/core/ARGenerationTest.cpp`:
- Around line 209-229: The current invalid-bound tests cover only
ARGenerationChatIterator; extend the test coverage for the generate and
streamGenerate APIs, including min_new_tokens greater than max_length, negative
min_new_tokens, and non-positive max_length. Add cases using the existing test
fixtures/helpers and assert each API rejects these inputs, preserving the
independent validation paths in generate and streamGenerate.

In `@tests/core/Qwen35BenchmarkHarnessTest.cpp`:
- Around line 76-82: Update
Qwen35BenchmarkHarnessTest.MacCaptureFailsClosedWhenTelemetryIsRequired so
non-Apple builds call GTEST_SKIP() instead of completing without assertions;
keep the existing telemetry validation assertion unchanged under __APPLE__.
- Around line 4-6: Update Qwen35BenchmarkHarnessTest.cpp to include the standard
<algorithm> header directly, alongside its existing includes, because the test
uses std::find; do not rely on benchmark_harness.hpp’s transitive include.

In `@tools/gdn-h1a-4b-r4/controller.py`:
- Around line 83-84: Update the argument parsing and validation around --cpuset
and --threads so these options can either meaningfully configure behavior or are
removed in favor of module constants. If retaining them, remove the
unconditional rejection of values other than "0-7" and 8, and document in their
help text that the r4 contract intentionally freezes those values.
- Around line 150-151: The token-count contract is duplicated as hard-coded
64/63 values across the producer and analyzer. In
tools/gdn-h1a-4b-r4/controller.py lines 150-151, pass
contract["generated_tokens"] to --max_new_tokens and derive decode_steps from
that same contract value at lines 238-239; in tools/gdn-h1a-4b-r4/analyze.py
lines 139-146, validate stats["generated_tokens"] and stats["decode_steps"]
against the corresponding contract fields and divide decode_duration_us by
contract["decode_steps"].

In `@tools/gdn-h1a-4b-r4/test_harness.py`:
- Line 222: Update the write_text construction in the test harness to use
iterable unpacking when appending the final line, replacing the list
concatenation that triggers Ruff RUF005 while preserving the existing output and
encoding.
- Around line 156-159: Add tests in test_harness.py covering the untested
analyzer dispositions: verify equal candidate and baseline prefill duration
returns RETAIN_INCUMBENT, and differing telemetry_before.ceiling_vector values
across positions return SCREENING_BLOCKED. Add controller validator tests
confirming parse_prompt("bad-format") raises argparse.ArgumentTypeError and
validate_args rejects threads values other than 8, using the existing fixture
and assertion patterns.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 21f9c523-4c60-4b88-9b6d-e9fbce140748

📥 Commits

Reviewing files that changed from the base of the PR and between 2c889d9 and 0cd3f65.

📒 Files selected for processing (13)
  • examples/qwen3_5/benchmark_harness.hpp
  • examples/qwen3_5/main.cpp
  • mllm/backends/cpu/kernels/common/gdn/gated_delta_net.cpp
  • mllm/models/ARGeneration.cpp
  • mllm/models/ARGeneration.hpp
  • tests/core/ARGenerationTest.cpp
  • tests/core/CMakeLists.txt
  • tests/core/Qwen35BenchmarkHarnessTest.cpp
  • tests/cpu/CMakeLists.txt
  • tests/cpu/Qwen35GDNConvTest.cpp
  • tools/gdn-h1a-4b-r4/analyze.py
  • tools/gdn-h1a-4b-r4/controller.py
  • tools/gdn-h1a-4b-r4/test_harness.py

Comment thread examples/qwen3_5/main.cpp Outdated
Comment thread examples/qwen3_5/main.cpp
Comment on lines +100 to +104
const std::filesystem::path jsonl_path(benchmark_jsonl.get());
std::error_code size_error;
if (std::filesystem::exists(jsonl_path) && std::filesystem::file_size(jsonl_path, size_error) != 0) {
throw std::invalid_argument("benchmark_jsonl must be new or empty");
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

size_error is never inspected, so failures report a misleading reason.

std::filesystem::file_size with an error_code returns static_cast<uintmax_t>(-1) on failure. A permission error or a directory at jsonl_path therefore produces "benchmark_jsonl must be new or empty", which hides the real cause. std::filesystem::exists(jsonl_path) without an error_code also throws filesystem_error instead of the intended invalid_argument. Check the status explicitly.

🐛 Proposed fix
       const std::filesystem::path jsonl_path(benchmark_jsonl.get());
-      std::error_code size_error;
-      if (std::filesystem::exists(jsonl_path) && std::filesystem::file_size(jsonl_path, size_error) != 0) {
-        throw std::invalid_argument("benchmark_jsonl must be new or empty");
+      std::error_code fs_error;
+      if (std::filesystem::exists(jsonl_path, fs_error)) {
+        const auto existing_size = std::filesystem::file_size(jsonl_path, fs_error);
+        if (fs_error) { throw std::invalid_argument("unable to stat benchmark_jsonl: " + fs_error.message()); }
+        if (existing_size != 0) { throw std::invalid_argument("benchmark_jsonl must be new or empty"); }
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/qwen3_5/main.cpp` around lines 100 - 104, Update the benchmark_jsonl
validation around jsonl_path to use error_code-aware filesystem checks, inspect
size_error immediately after file_size, and distinguish filesystem failures from
the non-empty-file case. Preserve the invalid_argument rejection only when the
path exists and has nonzero size, while reporting filesystem errors through the
existing error-handling approach.

Comment on lines +35 to +41
min_new_tokens_ = args.count("min_new_tokens") ? args.at("min_new_tokens").get<int>() : 0;
eos_token_id_ = args.count("eos_token_id") ? args.at("eos_token_id").get<int>() : gen.eos_token_id_;
do_sample_ = args.count("do_sample") ? args.at("do_sample").get<bool>() : gen.do_sample_;
if (max_length_ <= 0) { throw std::invalid_argument("max_length must be positive"); }
if (min_new_tokens_ < 0 || min_new_tokens_ > max_length_) {
throw std::invalid_argument("min_new_tokens must be between 0 and max_length");
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject invalid sampling temperatures before generation.

If temperature is negative, sampleTemperature multiplies the suppressed EOS logit by a negative value. This changes std::numeric_limits<float>::lowest() into the highest logit. EOS can then be sampled before min_new_tokens.

Reject non-finite and non-positive temperatures in all three entry points. Add a regression test with min_new_tokens > 1 and temperature = -1.0F.

Proposed fix
+#include <cmath>
 `#include` <limits>
 
-  if (max_length_ <= 0) { throw std::invalid_argument("max_length must be positive"); }
+  if (!(std::isfinite(temperature_) && temperature_ > 0.0F)) {
+    throw std::invalid_argument("temperature must be finite and positive");
+  }
+  if (max_length_ <= 0) { throw std::invalid_argument("max_length must be positive"); }
 
-  if (max_length <= 0) { throw std::invalid_argument("max_length must be positive"); }
+  if (!(std::isfinite(temperature) && temperature > 0.0F)) {
+    throw std::invalid_argument("temperature must be finite and positive");
+  }
+  if (max_length <= 0) { throw std::invalid_argument("max_length must be positive"); }
 
-  if (max_length <= 0) { throw std::invalid_argument("max_length must be positive"); }
+  if (!(std::isfinite(temperature) && temperature > 0.0F)) {
+    throw std::invalid_argument("temperature must be finite and positive");
+  }
+  if (max_length <= 0) { throw std::invalid_argument("max_length must be positive"); }

As per coding guidelines, “Validate inputs for public APIs and critical internal functions.”

Also applies to: 154-160, 235-241

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mllm/models/ARGeneration.cpp` around lines 35 - 41, Validate temperature in
all three generation entry points before sampling, rejecting non-finite or
non-positive values so invalid inputs cannot reach sampleTemperature; update the
relevant validation paths around the existing max_length/min_new_tokens checks.
Add a regression test covering min_new_tokens greater than 1 with temperature
set to -1.0F.

Source: Coding guidelines

Comment thread tests/core/CMakeLists.txt
Comment thread tests/cpu/Qwen35GDNConvTest.cpp
Comment thread tools/gdn-h1a-4b-r4/analyze.py Outdated
Comment on lines +268 to +274
try:
result = analyze(args.result_root)
except ContractError as error:
result = {"disposition": "HARNESS_REJECTED", "errors": [str(error)]}
args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(result["disposition"])
return 0 if result["disposition"] in ("HARNESS_REHEARSAL_VALID", "PROMOTE_H1A", "RETAIN_INCUMBENT", "REJECT_CORRECTNESS", "SCREENING_BLOCKED") else 2

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Only ContractError produces a receipt; other exceptions crash the analyzer.

analyze reaches unguarded dictionary and filesystem access on a malformed contract. Line 173 reads prompt["label"] and raises KeyError when a contract prompt omits the label. Line 198 sorts prompt.get("expected_tokens") and raises TypeError when a prompt omits the token count, because sorted compares None with int. Line 208 raises NotADirectoryError when a label names a regular file. In each case main propagates the exception, writes no output file, and prints no disposition. The fail-closed contract then produces no receipt. Catch Exception and record it as HARNESS_REJECTED.

🐛 Proposed fix
     try:
         result = analyze(args.result_root)
-    except ContractError as error:
-        result = {"disposition": "HARNESS_REJECTED", "errors": [str(error)]}
+    except ContractError as error:
+        result = {"disposition": "HARNESS_REJECTED", "errors": [str(error)]}
+    except Exception as error:  # noqa: BLE001 - the analyzer must always emit a receipt
+        result = {"disposition": "HARNESS_REJECTED", "errors": [f"{type(error).__name__}: {error}"]}
     args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try:
result = analyze(args.result_root)
except ContractError as error:
result = {"disposition": "HARNESS_REJECTED", "errors": [str(error)]}
args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(result["disposition"])
return 0 if result["disposition"] in ("HARNESS_REHEARSAL_VALID", "PROMOTE_H1A", "RETAIN_INCUMBENT", "REJECT_CORRECTNESS", "SCREENING_BLOCKED") else 2
try:
result = analyze(args.result_root)
except ContractError as error:
result = {"disposition": "HARNESS_REJECTED", "errors": [str(error)]}
except Exception as error: # noqa: BLE001 - the analyzer must always emit a receipt
result = {"disposition": "HARNESS_REJECTED", "errors": [f"{type(error).__name__}: {error}"]}
args.output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(result["disposition"])
return 0 if result["disposition"] in ("HARNESS_REHEARSAL_VALID", "PROMOTE_H1A", "RETAIN_INCUMBENT", "REJECT_CORRECTNESS", "SCREENING_BLOCKED") else 2
🧰 Tools
🪛 ast-grep (0.45.0)

[info] 271-271: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result, indent=2, sort_keys=True)
Note: [CWE-116] Improper Encoding or Escaping of Output.

(use-jsonify)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tools/gdn-h1a-4b-r4/analyze.py` around lines 268 - 274, Update the exception
handling in main around the analyze(args.result_root) call to catch general
Exception in addition to ContractError, converting any unexpected analysis
failure into the same HARNESS_REJECTED result with the exception message in
errors. Preserve the existing output-file writing, disposition printing, and
return-code logic for both handled error types.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@mllm/models/ARGeneration.hpp`:
- Around line 108-122: Expand the public comments for generate and its streaming
variant in ARGeneration.hpp to document the input and ARGenerationArgs
parameters, returned ARGenerationOutputPast or ARGenerationChatContext values,
callback invocation behavior, and all validation/error conditions. Cover both
APIs consistently, including the existing min_new_tokens/max_length contract,
and update the related declarations near the streaming API.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 25abffae-8f35-4c65-a651-e6406343fb36

📥 Commits

Reviewing files that changed from the base of the PR and between 0cd3f65 and 260e39e.

📒 Files selected for processing (7)
  • examples/qwen3_5/benchmark_harness.hpp
  • examples/qwen3_5/main.cpp
  • mllm/models/ARGeneration.hpp
  • tests/core/ARGenerationTest.cpp
  • tests/core/CMakeLists.txt
  • tests/core/Qwen35BenchmarkHarnessTest.cpp
  • tests/cpu/Qwen35GDNConvTest.cpp
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/core/ARGenerationTest.cpp
  • examples/qwen3_5/main.cpp
  • examples/qwen3_5/benchmark_harness.hpp
  • tests/cpu/Qwen35GDNConvTest.cpp
  • tests/core/Qwen35BenchmarkHarnessTest.cpp
  • tests/core/CMakeLists.txt

Comment thread mllm/models/ARGeneration.hpp
Compare the production kernel against an independent scalar reference on both
output and final history, bitwise. Covers batch {1,2}, sequence
{1,2,16,69,128,517}, channels {1,2,3,4,5,7,130} plus production 6144/8192 and
their channel tails, kernel sizes {2,3,4,5}, zero and non-zero initial history,
one-shot/split/multi-chunk partitions, reset between requests, and the null and
geometry guards.

All 12 tests pass against the unmodified kernel.
Process four adjacent channels per iteration when kernel_size is 4, which is
the production Qwen3.5 setting. vld3/vld4 deinterleave the [B, C, K-1] history
and [C, K] weights into per-tap lanes, so the per-token history shift happens
in registers instead of two scalar loads and two scalar stores per element.

Accumulation order matches the scalar body exactly: a rounded multiply by the
newest tap, then taps 0, 1, 2 fused in ascending order. Compilers already
contract the scalar accumulation into an FMA, so the vector form is bitwise
identical; the focused oracle asserts that rather than assuming it.

Non-AArch64 targets, kernel sizes other than 4, and channel tails keep the
incumbent scalar path. The public signature, layouts, and guards are unchanged.

Validated: focused conv oracle (1397 bitwise output+history checks) and the
GDN oracle pass on macOS arm64 with the NEON path active and on x86_64 with the
scalar fallback active; Android arm64-v8a builds with the NEON opcodes present
in libMllmCPUBackend.so.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/core/Qwen35BenchmarkHarnessTest.cpp`:
- Around line 58-63: Update the test around validateRequiredTelemetry to also
assert the error identifier for the missing ceiling_vector entry at index 2,
alongside the existing scaling_max_freq assertion, ensuring the test fails if
ceiling_vector validation is ignored.
- Around line 6-8: Update the includes in Qwen35BenchmarkHarnessTest to directly
include the standard headers required for std::string and std::vector, alongside
the existing includes, without relying on transitive dependencies.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7bb80ff2-4876-4c87-853f-7b36d326117e

📥 Commits

Reviewing files that changed from the base of the PR and between 2c889d9 and ef595c9.

📒 Files selected for processing (10)
  • examples/qwen3_5/benchmark_harness.hpp
  • examples/qwen3_5/main.cpp
  • mllm/backends/cpu/kernels/common/gdn/gated_delta_net.cpp
  • mllm/models/ARGeneration.cpp
  • mllm/models/ARGeneration.hpp
  • tests/core/ARGenerationTest.cpp
  • tests/core/CMakeLists.txt
  • tests/core/Qwen35BenchmarkHarnessTest.cpp
  • tests/cpu/CMakeLists.txt
  • tests/cpu/Qwen35GDNConvTest.cpp
🚧 Files skipped from review as they are similar to previous changes (8)
  • tests/core/CMakeLists.txt
  • tests/cpu/CMakeLists.txt
  • mllm/backends/cpu/kernels/common/gdn/gated_delta_net.cpp
  • tests/core/ARGenerationTest.cpp
  • examples/qwen3_5/benchmark_harness.hpp
  • examples/qwen3_5/main.cpp
  • mllm/models/ARGeneration.cpp
  • mllm/models/ARGeneration.hpp

Comment on lines +6 to +8
#include <algorithm>

#include "benchmark_harness.hpp"

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n 'std::(string|vector)|`#include` <(string|vector)>' tests/core/Qwen35BenchmarkHarnessTest.cpp

Repository: UbiquitousLearning/mllm

Length of output: 281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- top includes ---\n'
sed -n '1,30p' tests/core/Qwen35BenchmarkHarnessTest.cpp

printf '\n--- relevant include lines (string/vector) in repository headers used by test ---\n'
file="$(sed -n '8p' tests/core/Qwen35BenchmarkHarnessTest.cpp | awk '{print $2}' | tr -d '"<>')"
printf 'Direct included header: %s\n' "$file"
sed -n '1,80p' "$file" 2>/dev/null || true

printf '\n--- search for std::string/vector usages in test only ---\n'
rg -n 'std::(string|vector)|`#include` <(string|vector)>' tests/core/Qwen35BenchmarkHarnessTest.cpp

Repository: UbiquitousLearning/mllm

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- top includes ---'
sed -n '1,30p' tests/core/Qwen35BenchmarkHarnessTest.cpp

echo
echo '--- includes of std::string/std::vector in test only ---'
rg -n 'std::(string|vector)|`#include` <(string|vector)>' tests/core/Qwen35BenchmarkHarnessTest.cpp || true

Repository: UbiquitousLearning/mllm

Length of output: 1177


Include the standard-library dependencies directly.

This test uses std::string and std::vector, but it only gets them transitively through <gtest/gtest.h> or benchmark_harness.hpp. Add <string> and <vector> to avoid relying on header implementation details.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/core/Qwen35BenchmarkHarnessTest.cpp` around lines 6 - 8, Update the
includes in Qwen35BenchmarkHarnessTest to directly include the standard headers
required for std::string and std::vector, alongside the existing includes,
without relying on transitive dependencies.

Comment on lines +58 to +63
snapshot["cpus"][2]["scaling_max_freq"] = nullptr;
snapshot["ceiling_vector"][2] = nullptr;

const auto errors = mllm::examples::qwen3_5::benchmark::validateRequiredTelemetry(snapshot);
EXPECT_NE(std::find(errors.begin(), errors.end(), "cpu6_missing_scaling_max_freq"), errors.end());
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the missing ceiling-vector error.

The test sets ceiling_vector[2] to nullptr, but it asserts only "cpu6_missing_scaling_max_freq". The test passes if validateRequiredTelemetry ignores ceiling_vector. Assert the error that specifically reports the missing ceiling entry.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/core/Qwen35BenchmarkHarnessTest.cpp` around lines 58 - 63, Update the
test around validateRequiredTelemetry to also assert the error identifier for
the missing ceiling_vector entry at index 2, alongside the existing
scaling_max_freq assertion, ensuring the test fails if ceiling_vector validation
is ignored.

…ion tests

- ARGenerationTest: add generate/streamGenerate invalid-bound cases
  (min_new_tokens > max_length, negative min_new_tokens, non-positive
  max_length) covering the independent validation paths
- examples/qwen3_5/main.cpp: --benchmark_warmup alone now enters benchmark
  mode and triggers the required-option validation
- tests: include <algorithm>/<stdexcept> directly instead of relying on
  transitive includes; non-Apple builds GTEST_SKIP the macOS-only telemetry
  test
- benchmark_harness.hpp: document why cpu0/online is hard-coded online
- ARGeneration.hpp: document the min_new_tokens/max_length contract on the
  public generate/streamGenerate/chat declarations

Note: CTest registration via gtest_discover_tests was considered and dropped:
the repo registers no test target with CTest, and gtest_discover_tests runs
the test executable at build time, which cannot execute an Android ARM64
binary on the x86 CI runner (build-android failure).
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

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