Skip to content

feat(cpu): add Qwen3.5 0.8B mobile CPU support - #690

Merged
chenghuaWang merged 14 commits into
UbiquitousLearning:mainfrom
Aharrypotter:perf/qwen35-arm-cpu-gdn-phase3
Jul 29, 2026
Merged

feat(cpu): add Qwen3.5 0.8B mobile CPU support#690
chenghuaWang merged 14 commits into
UbiquitousLearning:mainfrom
Aharrypotter:perf/qwen35-arm-cpu-gdn-phase3

Conversation

@Aharrypotter

@Aharrypotter Aharrypotter commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Adds an end-to-end, text-only Qwen3.5-0.8B path for the CPU backend:

  • hybrid 18-layer Gated Delta Net (GDN) + 6-layer full-attention model
  • Qwen3.5 configuration, tokenizer, runner, and per-prompt state reset
  • lossless streaming UTF-8 output when one character spans multiple tokens
  • host-side KAI W4A32 packing and model-file v2 conversion
  • ARM CPU GDN execution with shared Q/K normalization and bounded (batch, value_head) parallelism

The vision tower and MTP layer are deliberately excluded.

Review guide

Area Main files What to review
Model contract mllm/models/qwen3_5/ 0.8B dimensions, hybrid layer schedule, partial RoPE, recurrent state lifecycle, tokenizer bytes
CPU kernels gated_delta_net.*, kai_w4a32_pack.*, ContiguousOp.cpp GDN recurrence, grouped heads, task partitioning, KAI packing, prefill copies
Conversion pymllm/mobile/convertor/, pymllm/mobile/quantize/ text-tower filtering, deterministic quantization planning, v2 format validation
Integration examples/qwen3_5/, tests/cpu/, tests/core/ conversion workflow, runner behavior, correctness and reset coverage

Suggested review order: model contract → GDN kernel → W4A32 conversion → runner/tests.

Current-head validation

Exact PR HEAD: 6cf4ea1e

Validated implementation HEAD: 0576fbde (the later commits only update the root README model table).

Gate Result
macOS arm64 / AppleClang 17 Qwen3.5 runner and all affected test targets built
Focused C++ tests 22/22 passed across GDN, KAI packing, tokenizer, config, Contiguous, and ARGeneration
Focused Python tests 3/3 passed for checkpoint filtering and model-file v2 boundaries
Static checks clang-format, Black, Ruff, Python compileall, and git diff --check passed
Runner smoke test mllm-qwen3-5-runner --help passed

Earlier H20, Android, and device runs predate the validated implementation HEAD and are therefore retained below as historical evidence, not presented as current-head validation.

Earlier cross-platform, correctness, and performance evidence
  • 896bfd45
    • H20 Linux x86-64 build produced the runner; 18/18 focused tests passed.
    • Android ARM64 NDK r28b build completed all 512 Ninja edges.
    • Pixel 9 Pro XL verified transferred artifact hashes; 19/19 focused tests passed.
  • e42ce170
    • all 85 prompt token IDs matched the official Qwen3.5-0.8B tokenizer
    • mllm F32 and Hugging Face BF16 matched for the first four greedy output token IDs
    • grouped-head coverage uses the official 4B/9B geometry (Hk=16, Hv=32)
  • KAI W4A32 vendor-reference coverage: 27 passed, 1 structurally empty case skipped, 0 failed.
  • Pixel ABBA campaign at e2492f22
    • identical prompt coverage and generated token IDs between baseline and candidate
    • prefill median: 2,839,778 us → 2,238,819.5 us (1.2684x, 21.16% lower)
  • The optimized GDN source blob is identical from measured commit e2492f22 through current HEAD 6cf4ea1e.

How to use it

examples/qwen3_5/README.md documents checkpoint validation, text-tower conversion, desktop/Android builds, and runner invocation.

Known limits

  • end-to-end model validation covers Qwen3.5-0.8B text inference only
  • Hk < Hv is covered by a focused test with official 4B/9B geometry, not by an end-to-end performance claim
  • exact W4 token parity with the BF16 reference is not claimed; the non-quantized F32 path has the four-token parity check above

Tracks #651.

Related context: #657. This implementation and its validation were developed independently; #657 was not used as a code baseline.

Summary by CodeRabbit

  • New Features
    • Added a Qwen3.5 0.8B CPU example/runner (interactive + one-shot), including new configuration and KAI W4A32 quantization recipes.
    • Added checkpoint and converted-model validation scripts, plus converter support for loading only selected tensor-name prefixes.
    • Added a CPU-side KAI W4A32 packing/quantization utility and CPU GDN kernel.
  • Bug Fixes
    • Improved contiguous-tensor copying and refined generation timing/performance reporting.
  • Documentation
    • Documented Qwen3.5 0.8B CPU usage and added it to the supported models table.
  • Tests
    • Added/expanded unit tests for Qwen3.5 tokenizer/config/GDN/KAI packing, contiguity, and AR generation stats.

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f809e26e-3ddd-4abc-b5c1-dcb7d8c46639

📥 Commits

Reviewing files that changed from the base of the PR and between 6cf4ea1 and 1d6fabb.

📒 Files selected for processing (2)
  • README.md
  • examples/qwen3_5/README.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • README.md
  • examples/qwen3_5/README.md

📝 Walkthrough

Walkthrough

Adds CPU Qwen3.5 hybrid inference with GDN and full-attention layers, tokenizer and CLI support, KAI quantization and conversion validation, generation metrics, mobile converter validation, contiguous-copy updates, and tests.

Changes

Qwen3.5 runtime and example

Layer / File(s) Summary
GDN and KAI CPU kernels
mllm/backends/cpu/kernels/common/gdn/*, mllm/backends/cpu/kernels/common/kai_w4a32_pack.*, mllm/backends/cpu/kernels/arm/linear/kai.cpp
Adds stateful GDN kernels, shared KAI W4A32 packing, and CPU build integration.
Qwen3.5 configuration, model, and tokenizer
mllm/models/qwen3_5/*
Adds configuration validation, tokenization, hybrid execution, KV caching, and state reset.
Runnable example and validation workflow
examples/qwen3_5/*, examples/CMakeLists.txt, README.md
Adds model recipes, conversion audits, documentation, and the Qwen3.5 CLI target.
Qwen3.5 tests
tests/cpu/Qwen35*, tests/cpu/KaiW4A32PackTest.cpp, tests/cpu/CMakeLists.txt
Adds kernel, packing, tokenizer, configuration, and determinism tests.

Conversion and model-file validation

Layer / File(s) Summary
Converter loading and CLI
pymllm/mobile/convertor/__init__.py, pymllm/mobile/utils/mllm_convertor.py
Adds prefix-filtered loading and stricter converter argument validation.
Quantization planning
pymllm/mobile/quantize/*
Centralizes pass planning and validates KAI inputs, outputs, replacements, and streaming behavior.
ModelFileV2 validation
pymllm/mobile/convertor/model_file_v2.py, pymllm/mobile/tests/test_convertor.py
Validates descriptors, tensor shapes, modes, supported types, parameter counts, and finalization.

Generation performance metrics

Layer / File(s) Summary
Performance contract and implementation
mllm/models/ARGeneration.hpp, mllm/models/ARGeneration.cpp
Adds structured generation statistics and updates timing, EOS, decode, and custom-event handling.
Performance tests
tests/core/ARGenerationTest.cpp, tests/core/CMakeLists.txt
Adds deterministic coverage for completion, EOS, timing, decode steps, and repeated requests.

Contiguous tensor operation

Layer / File(s) Summary
Contiguous paths and tests
mllm/backends/cpu/ops/ContiguousOp.cpp, tests/cpu/ContiguousOpTest.cpp, tests/cpu/CMakeLists.txt
Adds a full-copy fast path, revised strided offset calculation, and coverage for sliced, strided, and scalar tensors.

Mobile module loading

Layer / File(s) Summary
Lazy service import
pymllm/mobile/__init__.py
Loads and caches pymllm.mobile.service on first attribute access.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Qwen3_5Tokenizer
  participant Qwen3_5ForCausalLM
  participant Qwen3_5Text
  participant CPUKernels
  CLI->>Qwen3_5Tokenizer: convert prompt to token ids
  CLI->>Qwen3_5ForCausalLM: reset state and generate
  Qwen3_5ForCausalLM->>Qwen3_5Text: run hybrid decoder with KV cache
  Qwen3_5Text->>CPUKernels: execute GDN convolution and gated delta rule
  CPUKernels-->>Qwen3_5Text: return recurrent-layer outputs
  Qwen3_5Text-->>Qwen3_5ForCausalLM: return logits
  Qwen3_5ForCausalLM-->>CLI: stream generated tokens
Loading

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 5.84% 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
Title check ✅ Passed The title is concise and accurately reflects the main addition: Qwen3.5 0.8B CPU mobile support.
Description check ✅ Passed The description is detailed, on-topic, and includes behavior, review areas, validation, usage, and known limits.
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.
✨ 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.

@Aharrypotter

Copy link
Copy Markdown
Contributor Author

The Android, Linux x86_64, and macOS workflows for this fork PR are currently action_required and need maintainer approval before they can run. Exact-head/focused local, H20-hosted, NDK, and Android device evidence is summarized in the PR body.

@Aharrypotter

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
mllm/models/ARGeneration.cpp (1)

152-160: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

prefillEventStartTimePoint() zeroes ar_prefill_tokens_ right after it's set — batch generate() always reports 0 prefill tokens.

ar_prefill_tokens_ is assigned from past["sequence"].shape()[1] at line 155, then prefillEventStartTimePoint() is called at line 156, which unconditionally resets ar_prefill_tokens_ = 0 (see the rewritten helper, lines 534-546). The value set on line 155 is immediately discarded, so perfStats().prefill_tokens — and therefore prefill_tokens_per_sec in perfSummary() — will always read 0 for the batch generate() API.

Compare with the correct ordering used elsewhere in this same diff:

  • ARGenerationChatIterator::step() calls prefillEventStartTimePoint() first, then sets ar_prefill_tokens_ after forward() returns (lines 81, 90).
  • streamGenerate() does the same (lines 230, 239).

Only generate() has the assignment before the reset. This isn't caught by the new tests, since ARGenerationTest.cpp only exercises the chat() iterator path.

🐛 Proposed fix
     if (i == 0) {
-      if (past.count("sequence") > 0) { ar_prefill_tokens_ = past["sequence"].shape()[1]; }
       prefillEventStartTimePoint();
+      if (past.count("sequence") > 0) { ar_prefill_tokens_ = past["sequence"].shape()[1]; }
     } else {
       decodeEventStartTimePoint();
     }
🤖 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 152 - 160, Reorder the
first-iteration logic in generate() so prefillEventStartTimePoint() runs before
assigning ar_prefill_tokens_ from past["sequence"].shape()[1]. Preserve the
existing decode timing path and ensure the batch generate() API retains the
measured prefill token count for perfStats() and perfSummary().
🧹 Nitpick comments (10)
pymllm/mobile/__init__.py (1)

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

Document the module-level __getattr__ contract.

The docstring only describes the purpose; document the name parameter, returned module, and AttributeError raised for unsupported attributes.

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

Proposed docstring
 def __getattr__(name: str):
-    """Load optional subsystems only when a caller requests them."""
+    """Lazily load optional subsystems.
+
+    Args:
+        name: Requested module attribute.
+    Returns:
+        The requested optional subsystem module.
+    Raises:
+        AttributeError: If the attribute is not supported.
+    """
🤖 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 `@pymllm/mobile/__init__.py` around lines 13 - 19, Expand the module-level
__getattr__ docstring to document the name parameter, the lazily returned
service module, and the AttributeError raised for unsupported attribute names.
Keep the existing lazy import and caching behavior unchanged.

Source: Coding guidelines

pymllm/mobile/utils/mllm_convertor.py (1)

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

Redundant re-filtering: load_model already applies include_prefixes.

convertor.load_model(args.input_path, args.include_prefix) already filters tensors via its internal should_load() predicate using the exact same prefix-matching semantics. Re-filtering params here duplicates that logic; if the two implementations ever diverge, results could silently disagree.

♻️ Proposed simplification
     # Get params
     params = convertor.load_model(args.input_path, args.include_prefix)
     if args.include_prefix:
-        params = {
-            name: tensor
-            for name, tensor in params.items()
-            if any(name.startswith(prefix) for prefix in args.include_prefix)
-        }
         if not params:
             parser.error(
                 "--include_prefix did not match any parameters; refusing to write an empty model"
             )
         if args.verbose:
             print(
                 f"Kept {len(params)} parameters matching prefixes: "
                 + ", ".join(args.include_prefix)
             )
🤖 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 `@pymllm/mobile/utils/mllm_convertor.py` around lines 58 - 74, Remove the
redundant parameter filtering comprehension after convertor.load_model in the
conversion flow. Keep the empty-result validation and verbose reporting based on
the params returned by load_model, which already applies args.include_prefix
with the canonical matching logic.
mllm/ffi/Extension.cc (1)

383-383: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Move the mid-file include into the top-of-file include block.

Extension.cc keeps the normal include block at the top, but this header is still included inline before the registration block at line 383. Keep headers grouped at the top of the file.

🤖 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/ffi/Extension.cc` at line 383, Move the kai_w4a32_pack.hpp include from
its current mid-file location into Extension.cc’s existing top-of-file include
block, leaving the registration block and surrounding implementation unchanged.

Source: Coding guidelines

mllm/models/ARGeneration.cpp (1)

64-134: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Timing bookkeeping is duplicated near-identically across step(), generate(), and streamGenerate().

All three implement the same prefill/decode/first-token/EOS timing sequence with only minor per-API differences (device handling, callback vs. return). This duplication is exactly why the ordering bug in generate() (see the comment on lines 154-159) could slip in — the three copies drifted out of sync. Consider extracting a shared private helper (e.g., recordStepTiming(bool is_first_step, const ARGenerationOutputPast& pre_forward_input)) that all three call sites use, to guarantee a single source of truth for event ordering.

Also applies to: 138-211, 213-279

🤖 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 64 - 134, The timing bookkeeping
is duplicated across ARGenerationChatIterator::step(), generate(), and
streamGenerate(), allowing event ordering to diverge. Extract a shared private
helper for prefill, decode, first-token, and EOS timing, passing the step
context needed by each API, then replace each duplicated timing sequence with
calls to that helper while preserving their existing device, callback, and
return behavior.
tests/core/ARGenerationTest.cpp (1)

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

No test coverage for ARGeneration::generate() or streamGenerate() performance stats.

All new tests exercise only the chat() iterator path. This is why the ar_prefill_tokens_ reset-ordering bug in generate() (flagged in mllm/models/ARGeneration.cpp lines 154-159) wasn't caught. Consider adding at least one generate()-based test asserting stats.prefill_tokens matches the input sequence length.

🤖 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 1 - 133, Add a
performance-statistics test using ARGeneration::generate(), not only
runChat()/chat(), and assert the resulting perfStats().prefill_tokens equals the
input sequence length. Reuse FakeGeneration and makeInput, while preserving the
existing assertions and test coverage.
mllm/models/ARGeneration.hpp (1)

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

Document the new public stats API and mark perfStats() [[nodiscard]].

ARGenerationPerformanceStats and perfStats() are new public surface with no docstrings explaining field units/semantics (e.g., what valid/completed gate, _us suffix meaning), and static analysis flags the accessor as missing [[nodiscard]].

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

📝 Proposed fix
+// Snapshot of generation timing/counters produced by ARGeneration::perfStats().
+// All *_us fields are microseconds; `valid` is false until prefill, prefill-end,
+// and first-token events have all been recorded for the current request.
 struct ARGenerationPerformanceStats {
   bool valid = false;
   bool completed = false;
@@
-  ARGenerationPerformanceStats perfStats() const;
+  [[nodiscard]] ARGenerationPerformanceStats perfStats() const;

Also applies to: 109-110

🤖 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` around lines 27 - 38, Document the public
ARGenerationPerformanceStats struct and each field’s semantics, including that
valid/completed gate the measurements, _us fields are microseconds, and
token/step counters’ meanings. Add clear documentation for the public
perfStats() accessor covering its return value, then mark perfStats()
[[nodiscard]] so callers cannot ignore the result.

Sources: Coding guidelines, Linters/SAST tools

mllm/backends/cpu/kernels/common/kai_w4a32_pack.hpp (1)

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

Add docstrings for the new shared packing API.

KaiW4A32Tile, kaiW4A32TileFromName, kaiW4A32PackedSize, and kaiW4A32QuantizeAndPack are now a cross-module contract (consumed from mllm/ffi/Extension.cc and kai.cpp), but only a single top-of-file comment exists. Document parameter ordering (out_channels/in_channels), the meaning of tile_name, and the std::invalid_argument error contract for unsupported/invalid inputs.

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/backends/cpu/kernels/common/kai_w4a32_pack.hpp` around lines 9 - 25, Add
clear API documentation for KaiW4A32Tile and the kaiW4A32TileFromName,
kaiW4A32PackedSize, and kaiW4A32QuantizeAndPack declarations. Describe
tile_name, preserve out_channels/in_channels ordering, document return or output
behavior and relevant parameters, and state that unsupported or invalid inputs
throw std::invalid_argument.

Source: Coding guidelines

examples/qwen3_5/quant_cfg_0.8B_w4a32_kai.json (1)

80-105: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider leaving the GDN gate projections in float32.

in_proj_a / in_proj_b are [16, 1024] — negligible memory savings, but their outputs drive the decay/beta gates in the recurrent update, where 4-bit weight error accumulates across the sequence. Keeping them unquantized costs ~128 KB total and removes a plausible quality risk.

🤖 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/quant_cfg_0.8B_w4a32_kai.json` around lines 80 - 105, Remove
the quantization entries for the in_proj_a and in_proj_b patterns from the
configuration so these GDN gate projections remain in float32. Leave the
surrounding quantization rules unchanged.
mllm/models/qwen3_5/tokenization_qwen3_5.hpp (1)

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

Derive the placeholder length instead of hardcoding 12.

-    size_t pos = applied_string.find("{{{prompt}}}");
+    static constexpr std::string_view kPromptPlaceholder = "{{{prompt}}}";
+    size_t pos = applied_string.find(kPromptPlaceholder);
     if (pos == std::string::npos) { throw std::runtime_error("Qwen3.5 message template is missing the prompt placeholder"); }
-    applied_string.replace(pos, 12, message.prompt);
+    applied_string.replace(pos, kPromptPlaceholder.size(), message.prompt);
🤖 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/qwen3_5/tokenization_qwen3_5.hpp` around lines 255 - 257, Update
the placeholder replacement in the message-template logic around applied_string
and "{{{prompt}}}" to derive the replacement length from the placeholder string
itself rather than hardcoding 12, while preserving the existing
missing-placeholder validation and replacement behavior.
examples/qwen3_5/validate_checkpoint.py (1)

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

Underscore-prefixed helper is imported cross-module.

validate_converted_model.py line 16 imports _expected_text_shapes from this module, which contradicts the private naming. Renaming it to expected_text_shapes (and updating the import) makes the shared contract explicit.

🤖 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/validate_checkpoint.py` at line 18, Rename the helper
_expected_text_shapes to expected_text_shapes and update the corresponding
import and references in validate_converted_model.py, preserving its existing
behavior and signature.
🤖 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 92-95: Update the output loop over model.chat results to stop
using std::wcout for detokenized text. Convert
tokenizer.detokenize(step.cur_token_id) with preprocessor::wideString2Utf8String
and emit the resulting UTF-8 text via fmt::print, preserving token ID
diagnostics and flush behavior while keeping stdout narrow-oriented.

In `@mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp`:
- Around line 78-97: Add an ARM-only test that iterates over every tile name
supported by kaiW4A32TileFromName and compares its returned nr, kr, and sr
values with the corresponding ARM KAI ukernel get_nr, get_kr, and get_sr values.
Reuse the existing tile-to-ukernel mapping and test infrastructure, and retain
the current non-ARM determinism test unchanged.
- Around line 26-30: Update truncateToBFloat16 to convert the float bit pattern
to bfloat16 using round-to-nearest-even rather than simply shifting away the
lower 16 bits. Preserve the returned uint16_t representation and ensure the
conversion handles carry into the upper half correctly before the per-block
scale is passed with kai_dt_bf16.

In `@mllm/models/qwen3_5/configuration_qwen3_5.hpp`:
- Around line 127-158: Add [[nodiscard]] to the const accessor methods
rotary_dim(), isFullAttentionLayer(), numFullAttentionLayers(), and
numGDNLayers() in the configuration class, preserving their existing signatures
and behavior.
- Around line 74-78: Validate explicitly provided linear_impl_type values in the
configuration-loading logic before assigning the result from
aops::str2LinearImplTypes. If parsing an explicitly supplied value yields
LinearImplTypes::kDefault, reject the configuration with the existing
configuration-error mechanism instead of silently accepting it; preserve the
default behavior when the field is absent.

In `@mllm/models/qwen3_5/modeling_qwen3_5.hpp`:
- Line 561: Update the Qwen3_5 generation stopping logic around eos_token_id_ so
the configured Qwen3_5Config::eos_token_id is honored in addition to
im_end_token_id, allowing either token to terminate generation. Preserve the
existing im_end_token_id behavior while ensuring the parsed configuration value
is no longer unused.
- Line 249: Initialize the layer_idx_ member in Qwen3_5FullAttention with a safe
default value so default-constructed instances cannot pass an indeterminate
index to updateKVCache. Preserve Qwen3_5Text’s later assignment for explicitly
configured layers.

In `@mllm/models/qwen3_5/tokenization_qwen3_5.hpp`:
- Around line 144-152: Update the unmatched branch in the tokenization loop
around qwen3_5TokenizerMatchPattern so it emits the current single character as
its own piece before advancing pos. Preserve the existing matched-pattern
behavior and ensure every unmatched character remains in the token stream for
lossless byte-wise encoding.

In `@pymllm/mobile/convertor/__init__.py`:
- Around line 117-127: Update the torch checkpoint loading in load_model() to
call torch.load with weights_only=True, preserving map_location="cpu" and the
existing prefix filtering and return behavior.

In `@pymllm/mobile/quantize/solver.py`:
- Around line 92-104: Make the collision validation around planned_names
order-independent by separating name removal from output-name collision checks.
First process every replace=True payload in param_groups to remove its input
names from the planned set, then perform the existing output collision checks
and additions in a second pass, preserving the ValueError behavior for genuine
collisions.

In `@tests/cpu/ContiguousOpTest.cpp`:
- Around line 13-38: Add a rank-0 scalar tensor case to CPUContiguousOpTest,
exercising Tensor::contiguous() and validating the scalar shape and copied value
so the ndim == 0 fast path in CPUContiguousOp::forward is covered. Keep the
existing contiguous and strided-view assertions unchanged.

In `@tests/cpu/Qwen35GDNTest.cpp`:
- Around line 199-255: Restore the process-wide CPU thread count after
ParallelBatchValueHeadsMatchSerialBitwise completes. Capture the existing value
before setCpuOpThreads(kThreadCount), then restore it after the comparisons,
ensuring restoration also occurs if an assertion aborts the test path.

---

Outside diff comments:
In `@mllm/models/ARGeneration.cpp`:
- Around line 152-160: Reorder the first-iteration logic in generate() so
prefillEventStartTimePoint() runs before assigning ar_prefill_tokens_ from
past["sequence"].shape()[1]. Preserve the existing decode timing path and ensure
the batch generate() API retains the measured prefill token count for
perfStats() and perfSummary().

---

Nitpick comments:
In `@examples/qwen3_5/quant_cfg_0.8B_w4a32_kai.json`:
- Around line 80-105: Remove the quantization entries for the in_proj_a and
in_proj_b patterns from the configuration so these GDN gate projections remain
in float32. Leave the surrounding quantization rules unchanged.

In `@examples/qwen3_5/validate_checkpoint.py`:
- Line 18: Rename the helper _expected_text_shapes to expected_text_shapes and
update the corresponding import and references in validate_converted_model.py,
preserving its existing behavior and signature.

In `@mllm/backends/cpu/kernels/common/kai_w4a32_pack.hpp`:
- Around line 9-25: Add clear API documentation for KaiW4A32Tile and the
kaiW4A32TileFromName, kaiW4A32PackedSize, and kaiW4A32QuantizeAndPack
declarations. Describe tile_name, preserve out_channels/in_channels ordering,
document return or output behavior and relevant parameters, and state that
unsupported or invalid inputs throw std::invalid_argument.

In `@mllm/ffi/Extension.cc`:
- Line 383: Move the kai_w4a32_pack.hpp include from its current mid-file
location into Extension.cc’s existing top-of-file include block, leaving the
registration block and surrounding implementation unchanged.

In `@mllm/models/ARGeneration.cpp`:
- Around line 64-134: The timing bookkeeping is duplicated across
ARGenerationChatIterator::step(), generate(), and streamGenerate(), allowing
event ordering to diverge. Extract a shared private helper for prefill, decode,
first-token, and EOS timing, passing the step context needed by each API, then
replace each duplicated timing sequence with calls to that helper while
preserving their existing device, callback, and return behavior.

In `@mllm/models/ARGeneration.hpp`:
- Around line 27-38: Document the public ARGenerationPerformanceStats struct and
each field’s semantics, including that valid/completed gate the measurements,
_us fields are microseconds, and token/step counters’ meanings. Add clear
documentation for the public perfStats() accessor covering its return value,
then mark perfStats() [[nodiscard]] so callers cannot ignore the result.

In `@mllm/models/qwen3_5/tokenization_qwen3_5.hpp`:
- Around line 255-257: Update the placeholder replacement in the
message-template logic around applied_string and "{{{prompt}}}" to derive the
replacement length from the placeholder string itself rather than hardcoding 12,
while preserving the existing missing-placeholder validation and replacement
behavior.

In `@pymllm/mobile/__init__.py`:
- Around line 13-19: Expand the module-level __getattr__ docstring to document
the name parameter, the lazily returned service module, and the AttributeError
raised for unsupported attribute names. Keep the existing lazy import and
caching behavior unchanged.

In `@pymllm/mobile/utils/mllm_convertor.py`:
- Around line 58-74: Remove the redundant parameter filtering comprehension
after convertor.load_model in the conversion flow. Keep the empty-result
validation and verbose reporting based on the params returned by load_model,
which already applies args.include_prefix with the canonical matching logic.

In `@tests/core/ARGenerationTest.cpp`:
- Around line 1-133: Add a performance-statistics test using
ARGeneration::generate(), not only runChat()/chat(), and assert the resulting
perfStats().prefill_tokens equals the input sequence length. Reuse
FakeGeneration and makeInput, while preserving the existing assertions and test
coverage.
🪄 Autofix (Beta)

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: b70de051-ffae-44bb-9220-17089c52f9c2

📥 Commits

Reviewing files that changed from the base of the PR and between c4fd487 and e42ce17.

📒 Files selected for processing (34)
  • examples/CMakeLists.txt
  • examples/qwen3_5/CMakeLists.txt
  • examples/qwen3_5/README.md
  • examples/qwen3_5/config_0.8B_w4a32_kai.json
  • examples/qwen3_5/main.cpp
  • examples/qwen3_5/quant_cfg_0.8B_w4a32_kai.json
  • examples/qwen3_5/validate_checkpoint.py
  • examples/qwen3_5/validate_converted_model.py
  • mllm/backends/cpu/CMakeLists.txt
  • mllm/backends/cpu/kernels/arm/linear/kai.cpp
  • mllm/backends/cpu/kernels/common/gdn/gated_delta_net.cpp
  • mllm/backends/cpu/kernels/common/gdn/gated_delta_net.hpp
  • mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp
  • mllm/backends/cpu/kernels/common/kai_w4a32_pack.hpp
  • mllm/backends/cpu/ops/ContiguousOp.cpp
  • mllm/ffi/Extension.cc
  • mllm/models/ARGeneration.cpp
  • mllm/models/ARGeneration.hpp
  • mllm/models/qwen3_5/configuration_qwen3_5.hpp
  • mllm/models/qwen3_5/modeling_qwen3_5.hpp
  • mllm/models/qwen3_5/tokenization_qwen3_5.hpp
  • pymllm/mobile/__init__.py
  • pymllm/mobile/convertor/__init__.py
  • pymllm/mobile/convertor/model_file_v2.py
  • pymllm/mobile/quantize/kai/w4a32.py
  • pymllm/mobile/quantize/solver.py
  • pymllm/mobile/utils/mllm_convertor.py
  • tests/core/ARGenerationTest.cpp
  • tests/core/CMakeLists.txt
  • tests/cpu/CMakeLists.txt
  • tests/cpu/ContiguousOpTest.cpp
  • tests/cpu/KaiW4A32PackTest.cpp
  • tests/cpu/Qwen35GDNTest.cpp
  • tests/cpu/Qwen35TokenizerTest.cpp

Comment thread examples/qwen3_5/main.cpp
Comment on lines +26 to +30
uint16_t truncateToBFloat16(float value) {
uint32_t bits = 0;
std::memcpy(&bits, &value, sizeof(bits));
return static_cast<uint16_t>(bits >> 16);
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file location =="
git ls-files | rg 'mllm/backends/cpu/kernels/common/kai_w4a32_pack\.cpp|kai_w4a32_pack' || true

echo "== inspect target file =="
if [ -f mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp ]; then
  nl -ba mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp | sed -n '1,120p'
fi

echo "== search related bf16 helpers/usages =="
rg -n "truncateToBFloat16|roundToBFloat16|cast_bf16_f32|kai_cast_bf16_f32|BFloat16|bf16|float_to_bf16" mllm pymllm mllm-cli 2>/dev/null || true

echo "== deterministic f32->bf16 compare for small range =="
python3 - <<'PY'
import struct
def trunc_bf16(value):
    bits = struct.unpack('<I', struct.pack('<f', value))[0]
    return bits >> 16

def rnd_bf16(value):
    bits = struct.unpack('<I', struct.pack('<f', value))[0]
    rounding_bias = ((bits >> 16) & 1U) + 0x7FFFU
    out = (bits + rounding_bias) >> 16
    # Handle inf/nan preserved through arithmetic with bias? keep same as trunc?
    return out

def uf16_to_float(uf16):
    return struct.unpack('<f', struct.pack('<I', uf16 << 16))[0]

values = list(struct.unpack("<2147483648f", bytes([0x7f]*4)))[::2147483647//2] if False else []
samples = [
    1.5, 2.5, 3.5, 4.5, 5.5,
    -1.5, -2.5, -3.5, -4.5, -5.5,
    0.5, 1.0, 7.0, 8.0, 16.0,
    0.000977517, 0.000977518,
]
for v in samples:
    b_trunc = trunc_bf16(v)
    b_rnd = rnd_bf16(v)
    print(v, u"{:016b}".format(b_trunc), uf16_to_float(b_trunc), u"{:016b}".format(b_rnd), uf16_to_float(b_rnd))
PY

Repository: UbiquitousLearning/mllm

Length of output: 351


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file location =="
git ls-files | rg 'mllm/backends/cpu/kernels/common/kai_w4a32_pack\.cpp|kai_w4a32_pack' || true

echo "== inspect target file =="
if [ -f mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp ]; then
  nl -ba mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp | sed -n '1,120p'
fi

echo "== search related bf16 helpers/usages =="
rg -n "truncateToBFloat16|roundToBFloat16|cast_bf16_f32|kai_cast_bf16_f32|BFloat16|bf16|float_to_bf16" mllm pymllm mllm-cli 2>/dev/null || true

echo "== deterministic f32->bf16 compare for selected cases =="
python3 - <<'PY'
import struct

def trunc_bf16(value):
    bits = struct.unpack('<I', struct.pack('<f', value))[0]
    return bits >> 16

def rnd_bf16(value):
    bits = struct.unpack('<I', struct.pack('<f', value))[0]
    rounding_bias = ((bits >> 16) & 1U) + 0x7FFFU
    return (bits + rounding_bias) >> 16

def uf16_to_float(uf16):
    return struct.unpack('<f', struct.pack('<I', uf16 << 16))[0]

samples = [
    1.5, 2.5, 3.5, 4.5, 5.5,
    -1.5, -2.5, -3.5, -4.5, -5.5,
    0.5, 1.0, 7.0, 8.0, 16.0,
    0.000977517, 0.000977518,
]
for v in samples:
    b_trunc = trunc_bf16(v)
    b_rnd = rnd_bf16(v)
    print(v, f"{b_trunc:016b}", uf16_to_float(b_trunc), f"{b_rnd:016b}", uf16_to_float(b_rnd))
PY

Repository: UbiquitousLearning/mllm

Length of output: 351


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect target file =="
if [ -f mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp ]; then
  awk '{ printf "%6d\t%s\n", NR, $0 }' mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp | sed -n '1,140p'
fi

echo "== inspect header (if relevant) =="
if [ -f mllm/backends/cpu/kernels/common/kai_w4a32_pack.hpp ]; then
  awk '{ printf "%6d\t%s\n", NR, $0 }' mllm/backends/cpu/kernels/common/kai_w4a32_pack.hpp | sed -n '1,140p'
fi

echo "== search related bf16 helpers/usages =="
rg -n "truncateToBFloat16|roundToBFloat16|cast_bf16_f32|kai_cast_bf16_f32|BFloat16|bf16|float_to_bf16" mllm pymllm mllm-cli 2>/dev/null || true

echo "== deterministic f32->bf16 compare for selected cases =="
python3 - <<'PY'
import struct

def trunc_bf16(value):
    bits = struct.unpack('<I', struct.pack('<f', value))[0]
    return bits >> 16

def rnd_bf16(value):
    bits = struct.unpack('<I', struct.pack('<f', value))[0]
    rounding_bias = ((bits >> 16) & 1U) + 0x7FFFU
    return (bits + rounding_bias) >> 16

def uf16_to_float(uf16):
    return struct.unpack('<f', struct.pack('<I', uf16 << 16))[0]

samples = [
    1.5, 2.5, 3.5, 4.5, 5.5,
    -1.5, -2.5, -3.5, -4.5, -5.5,
    0.5, 1.0, 7.0, 8.0, 16.0,
    0.000977517, 0.000977518,
]
for v in samples:
    b_trunc = trunc_bf16(v)
    b_rnd = rnd_bf16(v)
    print(v, f"{b_trunc:016b}", uf16_to_float(b_trunc), f"{b_rnd:016b}", uf16_to_float(b_rnd))
PY

Repository: UbiquitousLearning/mllm

Length of output: 11009


🌐 Web query:

KleidiAI kai_cast_bf16_f32 implementation bf16 conversion

💡 Result:

KleidiAI does not include a function specifically named kai_cast_bf16_f32 for BF16 to F32 conversion [1][2][3]. Instead, KleidiAI is primarily a library of performance-critical micro-kernels for operations like matrix multiplication and convolution [1][4]. In the context of KleidiAI, BF16-to-F32 or BF16-based operations are handled through specific operator micro-kernels, particularly those designed for GEMM (General Matrix Multiply) that target BF16 input and F32 output [5][3]. For instance, KleidiAI provides kernels such as matmul_clamp_f32_bf16p_bf16p, which perform matrix multiplication of two BF16 matrices and produce an F32 output [5]. These kernels are integrated into frameworks (like ONNX Runtime or llama.cpp) where the framework handles the necessary data preparation, packing, and invocation of the micro-kernel [6][7][8]. If you are looking to perform raw BF16-to-F32 conversion on Arm CPUs, you would typically use standard Arm instruction set features (such as those available via Neon or SVE/SME) rather than a specific KleidiAI library function, as KleidiAI focuses on optimized computational routines rather than general-purpose data format conversion utilities [1][4]. For example, developers using Arm architectures often utilize VCVT instructions (Neon) or equivalent SVE/SME conversion instructions to handle BF16 and F32 data types [1]. In summary: 1. No direct kai_cast_bf16_f32 API exists in KleidiAI [1][2][3]. 2. BF16/F32 functionality is implemented within dedicated matmul/operator micro-kernels where conversion is implicit in the operation [5][3]. 3. For general-purpose conversion, refer to Arm architecture reference manuals for the relevant conversion instructions (e.g., vcvt for Neon/SVE) [1].

Citations:


🌐 Web query:

ARM SVE bf16 f32 to bf16 rounding semantics fcvtns bf16 f32

💡 Result:

In the Arm AArch64 and SVE architectures, there is no instruction named fcvtns for converting floating-point to BFloat16. The instruction commonly used for single-precision (f32) to BFloat16 (bf16) conversion is BFCVT (or BFCVTN) [1][2][3]. The term fcvtns specifically refers to a different class of instructions: Floating-point Convert to Signed integer, with Rounding to Nearest, ties to Even [4][5]. It is not used for floating-point to floating-point conversions like f32 to bf16 [4][6][5]. Regarding BFloat16 conversion semantics: 1. Instruction Selection: For f32 to bf16 conversion, developers use BFCVT (predicated in SVE, scalar in AArch64) [2][3] or BFCVTN (narrowing, vector) [1]. 2. Rounding Semantics: The BFCVT instruction performs the conversion from single-precision to BFloat16 format using the FPConvertBF shared function [1][2][3]. According to official Arm documentation, BFCVT (and the related BFMLAL instructions) honors standard floating-point controls, operating much closer to full IEEE-FP32 accuracy than some other dedicated BF16 matrix-multiply instructions (like BFDOT or BFMMLA, which use a non-IEEE Round to Odd mode) [7]. 3. Contrast with fcvtns: Because fcvtns is an integer conversion instruction, it utilizes the standard FPCR (Floating-point Control Register) rounding modes (including Round to Nearest, ties to even) for converting floating-point values to fixed-point integers [4][6][5]. It is functionally distinct from the BFCVT family of instructions [1][2]. In summary, do not use fcvtns for f32 to bf16 conversion, as it will perform an integer conversion. Use BFCVT or BFCVTN for the intended floating-point narrowing operation [1][2].

Citations:


Round bf16 scale conversions instead of truncating.

truncateToBFloat16 discards the low 16 bits of the f32 bit pattern, so values like 2.5f dequantify to 4/8 instead of the canonical bf16 encodings 16/32. The scale produced here is written per W4A32 block and passed to KleidiAI with scale_dt = kai_dt_bf16, so use an f32→bf16 conversion with round-to-nearest-even semantics.

🤖 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/backends/cpu/kernels/common/kai_w4a32_pack.cpp` around lines 26 - 30,
Update truncateToBFloat16 to convert the float bit pattern to bfloat16 using
round-to-nearest-even rather than simply shifting away the lower 16 bits.
Preserve the returned uint16_t representation and ensure the conversion handles
carry into the upper half correctly before the per-block scale is passed with
kai_dt_bf16.

Comment on lines +78 to +97
KaiW4A32Tile kaiW4A32TileFromName(std::string_view tile_name) {
if (tile_name == "qai8dxp1x8_qsi4c32p4x8_1x4x32" || tile_name == "qai8dxp4x8_qsi4c32p4x8_8x4x32"
|| tile_name == "qai8dxp4x8_qsi4c32p4x8_16x4x32") {
return {.nr = 4, .kr = 16, .sr = 2};
}
if (tile_name == "qai8dxp1x8_qsi4c32p8x8_1x8x32" || tile_name == "qai8dxp4x8_qsi4c32p8x8_4x8x32") {
return {.nr = 8, .kr = 16, .sr = 2};
}
if (tile_name == "qai8dxp1x4_qsi4c32p4x4_1x4") { return {.nr = 4, .kr = 8, .sr = 2}; }
throw std::invalid_argument("Unsupported KAI W4A32 tile configuration: " + std::string(tile_name));
}

size_t kaiW4A32PackedSize(size_t out_channels, size_t in_channels, std::string_view tile_name) {
if (out_channels == 0 || in_channels == 0 || (in_channels % kBlockLength) != 0) {
throw std::invalid_argument("KAI W4A32 requires non-zero dimensions and input channels divisible by 32");
}
const auto tile = kaiW4A32TileFromName(tile_name);
return kai_get_rhs_packed_size_rhs_pack_nxk_qsi4c32p_qsu4c32s1s0(out_channels, in_channels, tile.nr, tile.kr, tile.sr,
kBlockLength, kai_dt_bf16);
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate kai.cpp files =="
fd -a 'kai.*\.cpp|kai.*\.h' . | sed 's#^\./##' | head -100

echo "== locate target file and related includes =="
for f in mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp mllm/backends/cpu/kernels/common/kai.cpp; do
  if [ -f "$f" ]; then
    echo "--- $f line count ---"
    wc -l "$f"
    echo "--- includes/outline ---"
    ast-grep outline "$f" --view compact || true
    echo "--- relevant sections ---"
    rg -n "kaiW4a32|W4A32|kai_get_rhs_packed_size_rhs_pack_nxk|get_nr|get_sr|get_cr|tile_name|kai_get_nr_k|kai_get_sr_k|kai_get_cr_k|tile" "$f" -C 3 -A 80 -B 20 | head -360
  fi
done

echo "== search for W4A32 tile definitions in repo =="
rg -n "qai8dxp1x8|qai8dxp4x8|qai8dxp1x4|W4A32|kai_get_rhs_packed_size_rhs_pack_nxk_qsi4c32p|get_rhs_packed_size|nr.*kr.*sr|kai_get_nr|kai_get_sr|kai_get_cr" . -g '*.cpp' -g '*.h' -g '*.hpp' | head -300

Repository: UbiquitousLearning/mllm

Length of output: 46720


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate kai.cpp files =="
fd -a 'kai.*\.cpp|kai.*\.h' . | sed 's#^\./##' | head -100

echo "== locate target file and related includes =="
for f in mllm/backends/cpu/kernels/common/kai_w4a32_pack.cpp mllm/backends/cpu/kernels/common/kai.cpp; do
  if [ -f "$f" ]; then
    echo "--- $f line count ---"
    wc -l "$f"
    echo "--- includes/outline ---"
    ast-grep outline "$f" --view compact || true
    echo "--- relevant sections ---"
    rg -n "kaiW4a32|W4A32|kai_get_rhs_packed_size_rhs_pack_nxk|get_nr|get_sr|get_cr|tile_name|kai_get_nr_k|kai_get_sr_k|kai_get_cr_k|tile" "$f" -C 3 -A 120 -B 40 | head -500
  fi
done

echo "== search for W4A32 tile definitions in repo =="
rg -n "qai8dxp1x8|qai8dxp4x8|qai8dxp1x4|W4A32|kai_get_rhs_packed_size_rhs_pack_nxk_qsi4c32p|get_rhs_packed_size|nr.*kr.*sr|kai_get_nr|kai_get_sr|kai_get_cr" . -g '*.cpp' -g '*.h' -g '*.hpp' | head -300

Repository: UbiquitousLearning/mllm

Length of output: 47526


Add an ARM cross-check for W4A32 tile parameters.

kaiW4A32TileFromName hardcodes nr/kr/sr for all supported W4A32 tiles, while the ARM ukernels expose get_nr/get_kr/get_sr for the same tile names. The existing non-ARM determinism test only exercises one tile; add an ARM-only check over every tile so the packed-size/packing parameters cannot drift from the real KAI microkernel values after a KleidiAI update.

🤖 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/backends/cpu/kernels/common/kai_w4a32_pack.cpp` around lines 78 - 97,
Add an ARM-only test that iterates over every tile name supported by
kaiW4A32TileFromName and compares its returned nr, kr, and sr values with the
corresponding ARM KAI ukernel get_nr, get_kr, and get_sr values. Reuse the
existing tile-to-ukernel mapping and test infrastructure, and retain the current
non-ARM determinism test unchanged.

Comment thread mllm/models/qwen3_5/configuration_qwen3_5.hpp
Comment thread mllm/models/qwen3_5/configuration_qwen3_5.hpp Outdated
Comment thread mllm/models/qwen3_5/tokenization_qwen3_5.hpp
Comment thread pymllm/mobile/convertor/__init__.py
Comment thread pymllm/mobile/quantize/solver.py
Comment thread tests/cpu/ContiguousOpTest.cpp Outdated
Comment thread tests/cpu/Qwen35GDNTest.cpp
@Aharrypotter

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@Aharrypotter
Aharrypotter marked this pull request as ready for review July 29, 2026 00:23

@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/qwen3_5/tokenization_qwen3_5.hpp`:
- Around line 19-28: Document the public API for Qwen3_5StreamingUtf8Decoder and
Qwen3_5Tokenizer, including each public constructor and method in the referenced
ranges. Describe class purpose, parameter and return semantics,
streaming/replacement behavior, and every runtime_error condition; ensure
comments accurately distinguish append, finish, reset, and tokenizer operations.
🪄 Autofix (Beta)

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: 37c0d66e-8397-4489-b89f-51a29f28906f

📥 Commits

Reviewing files that changed from the base of the PR and between 896bfd4 and 0576fbd.

📒 Files selected for processing (7)
  • examples/qwen3_5/main.cpp
  • mllm/models/qwen3_5/configuration_qwen3_5.hpp
  • mllm/models/qwen3_5/tokenization_qwen3_5.hpp
  • pymllm/mobile/tests/test_convertor.py
  • tests/cpu/CMakeLists.txt
  • tests/cpu/Qwen35ConfigTest.cpp
  • tests/cpu/Qwen35TokenizerTest.cpp
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/qwen3_5/main.cpp
  • mllm/models/qwen3_5/configuration_qwen3_5.hpp

Comment on lines +19 to +28
class Qwen3_5StreamingUtf8Decoder {
public:
std::string append(std::string_view bytes) {
if (!bytes.empty()) { pending_.append(bytes.data(), bytes.size()); }
return drain(false);
}

std::string finish() { return drain(true); }

void reset() { pending_.clear(); }

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

Document the public tokenizer APIs.

Qwen3_5StreamingUtf8Decoder and Qwen3_5Tokenizer expose undocumented classes and methods, including replacement behavior and runtime_error paths. Add API comments covering purpose, parameters, returns, and errors. As per coding guidelines, “Ensure public APIs, classes, and functions have clear docstrings or comments explaining purpose, parameters, returns, and errors.”

Also applies to: 242-250, 284-367

🤖 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/qwen3_5/tokenization_qwen3_5.hpp` around lines 19 - 28, Document
the public API for Qwen3_5StreamingUtf8Decoder and Qwen3_5Tokenizer, including
each public constructor and method in the referenced ranges. Describe class
purpose, parameter and return semantics, streaming/replacement behavior, and
every runtime_error condition; ensure comments accurately distinguish append,
finish, reset, and tokenizer operations.

Source: Coding guidelines

@chenghuaWang chenghuaWang left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@chenghuaWang
chenghuaWang merged commit bd7bcf7 into UbiquitousLearning:main Jul 29, 2026
4 checks passed
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.

2 participants