Skip to content

feat(cpu): add Qwen3.5 4B mobile CPU support - #691

Merged
chenghuaWang merged 2 commits into
UbiquitousLearning:mainfrom
Aharrypotter:feat/qwen35-4b-mobile-cpu
Jul 31, 2026
Merged

feat(cpu): add Qwen3.5 4B mobile CPU support#691
chenghuaWang merged 2 commits into
UbiquitousLearning:mainfrom
Aharrypotter:feat/qwen35-4b-mobile-cpu

Conversation

@Aharrypotter

@Aharrypotter Aharrypotter commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

This adds Qwen3.5-4B text-only support to the existing mllm v2 mobile CPU
path. It reuses the Qwen3.5 GDN/full-attention runtime and KAI Linear path
already used by 0.8B; this PR does not add a separate 4B kernel or quantization
format.

Review map

  1. Model contract: config_4B_w4a32_kai.json and
    quant_cfg_4B_w4a32_kai.json
  2. Conversion safety: checkpoint/V2 validators, including offsets above
    4 GiB
  3. Runtime integration: fail-fast config/model matching, focused tests,
    runner documentation, and the supported-model table

Supported contract

Surface Qwen3.5-4B
Scope Text tower only
Geometry 32 layers; hidden size 2560; intermediate size 9216
Layer schedule 24 GDN + 8 full-attention layers
Full attention 16 query heads; 4 KV heads; head dimension 256
GDN 16 key heads; 32 value heads; key/value head dimension 128
Runtime envelope Batch size 1; cache length up to 2048 tokens
Quantization W4A8 through the existing KAI path

The runner accepts only the official 0.8B and 4B runtime contracts and checks
the model embedding descriptor before constructing the model or its eager KV
cache.

Validation

The completed H20 final03 gates are bound to an exact 1,865-entry source
manifest verified before and after the run.

Gate Status Evidence
Pinned checkpoint contract audit PASS Qwen/Qwen3.5-4B pinned revision; 426 selected text tensors; 249/249 KAI targets
Converted-model V2 structural audit PASS 4,923,030,836 bytes; 427 descriptors (249 KAI-packed + 178 FP32); offsets cross 4 GiB; 0 vision/MTP descriptors in the converted artifact
H20 Python focused tests PASS Validators 4/4; converter tests 4/4
H20 Linux build and focused tests PASS GDN 6, KAI packing 2, tokenizer 4, config 4, contiguous 2: 18/18 total; runner build and --help also passed
Android NDK cross-build (build-only) PASS NDK r28b (28.1.13356709), API 28, arm64-v8a; runner, five focused test binaries, and required shared libraries passed AArch64 ELF/dependency checks

The pinned V2 artifact was generated by the audited conversion run and
revalidated by final03; final03 did not regenerate it. The Android result is
cross-build evidence, not device-execution evidence.

Exact evidence identities
  • PR commit:
    b27f801c0bee0b880e5a0298611a3c2c70c7bae8
  • final03 source manifest SHA-256:
    d8fb8a6307e48c514ddb61c5b853a63224ff2e9fc9ab12b27844befc48187f6e
  • Official checkpoint revision:
    851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a
  • Audited V2 artifact SHA-256:
    79b130ce333c44c15e7420f7dc3ce42badf374358aefe9be7acd38f5bd3d13e6

Pixel 9 Pro XL gates

Gate Status
Focused native tests PASS — 19/19 — config 4, GDN 6, KAI packing 3, tokenizer 4, contiguous 2; device artifacts passed pre/post SHA checks
Full-model Qwen3.5-4B inference PASS — exact 4,923,030,836-byte model SHA verified; the final03 runner handled two identical one-token prompts in one process, producing the same token ID after each state reset; strict UTF-8 and no error marker

Both gates used the artifact-bound final03 device bundle. These are focused
correctness/safety gates, not numerical-parity or performance claims.

Quantization naming

The user-facing contract is W4A8: KAI Linear dynamically quantizes
activations to INT8 and uses packed INT4 weights while retaining FP32 operator
inputs and outputs.

The existing w4a32_kai config names, model filename convention, and
w4a32_kai_pipeline identifier remain unchanged for compatibility.

Out of scope

  • Vision tower and MTP
  • Other Qwen3.5 sizes
  • Batch sizes greater than 1
  • Cache lengths greater than 2048 tokens
  • Performance optimization or benchmark claims

Tracks #651.

Extends the Qwen3.5-0.8B CPU support merged in #690.

Summary by CodeRabbit

  • New Features

    • Added guidance and workflows to run Qwen3.5 0.8B and 4B on ARM CPUs with W4A8 quantization.
    • Added model-specific runtime configurations and conversion/validation auditing.
    • Improved runtime validation for prompts, generation limits, and model/config compatibility.
  • Documentation

    • Expanded Qwen3.5 example docs with a model comparison, W4A8 quantization setup, and end-to-end convert/validate/run steps.
    • Updated the supported models list to include Qwen3.5-4B.
  • Bug Fixes

    • Improved handling of large parameter offsets during conversion and validation.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Qwen3.5 support now covers 0.8B and 4B ARM CPU workflows, with official runtime-contract checks, KAI quantization validation, converted-model descriptor checks, updated configurations, runner validation, documentation, and expanded tests.

Changes

Qwen3.5 runtime and conversion support

Layer / File(s) Summary
Runtime contracts and model startup
mllm/models/qwen3_5/..., examples/qwen3_5/main.cpp, tests/cpu/...
Runtime configuration parsing, official 0.8B/4B contract checks, parameter matching, startup validation, and C++ coverage are added.
Checkpoint and converted-model validation
examples/qwen3_5/validate_checkpoint.py, examples/qwen3_5/validate_converted_model.py, examples/qwen3_5/test_validators.py, pymllm/mobile/tests/test_convertor.py
Checkpoint variant resolution, KAI recipe validation, descriptor-table validation, metadata reporting, and large-offset coverage are added.
Model artifacts and example workflow
examples/qwen3_5/*.json, examples/qwen3_5/README.md, README.md
The 4B runtime and quantization configurations are added, and documentation covers conversion, validation, and execution for both model sizes.

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

Possibly related PRs

Suggested reviewers: chenghuawang, ubiquitouslearning

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.81% 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 clearly summarizes the main change: Qwen3.5-4B mobile CPU support.
Description check ✅ Passed The description is detailed and on-topic, covering summary, contract, validation, and out-of-scope items.
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
Aharrypotter marked this pull request as ready for review July 29, 2026 10:20

@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

🧹 Nitpick comments (2)
examples/qwen3_5/validate_converted_model.py (1)

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

Shared uint32-offset boundary duplicated as an unlinked literal/constant across two files. validate_converted_model.py hardcodes 1 << 32 inline for crosses_uint32_offset, while test_validators.py independently defines UINT32_LIMIT = 1 << 32 to assert the same boundary — a single exported constant would prevent the two from silently drifting apart.

  • examples/qwen3_5/validate_converted_model.py#L215-L239: define a module-level UINT32_LIMIT = 1 << 32 constant and use it in the crosses_uint32_offset computation instead of the inline literal.
  • examples/qwen3_5/test_validators.py#L32-L34: import UINT32_LIMIT from validate_converted_model instead of redefining it locally.
🤖 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_converted_model.py` around lines 215 - 239,
Centralize the uint32 boundary as a module-level UINT32_LIMIT in
examples/qwen3_5/validate_converted_model.py:215-239 and use it for
crosses_uint32_offset instead of the inline 1 << 32 literal. In
examples/qwen3_5/test_validators.py:32-34, import UINT32_LIMIT from
validate_converted_model and remove the local duplicate definition.
examples/qwen3_5/test_validators.py (1)

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

Consider testing the coverage-mismatch and tied-embedding-violation branches of validate_kai_recipe_contract.

Existing tests cover hint mismatches and linear_impl_type resolution well, but the "missing/unexpected linear" coverage check and the tied-embedding replace/rename enforcement (both critical safety checks in validate_checkpoint.py) aren't exercised by a negative test here.

🤖 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/test_validators.py` around lines 158 - 214, Extend
test_kai_recipe_contract_rejects_runtime_incompatible_hints to add negative
cases for validate_kai_recipe_contract’s missing/unexpected-linear coverage
validation and tied-embedding replace/rename enforcement. Mutate the loaded
quantization or runtime configuration minimally to trigger each branch, assert
AssertionError, and keep the existing hint and linear_impl_type checks
unchanged.
🤖 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/validate_checkpoint.py`:
- Around line 314-322: Guard the runtime configuration validation around
resolve_model_size so a missing runtime_config["text_config"] produces the
tool’s descriptive AssertionError instead of a raw KeyError. Update the
validation block after the model_type check, preserving the existing
runtime/checkpoint size comparison for configurations that include text_config.

In `@mllm/models/qwen3_5/configuration_qwen3_5.hpp`:
- Around line 182-255: Add documentation comments for the exported helpers
hasOfficialLayerSchedule, hasOfficialCommonRuntimeContract,
isOfficialQwen35_08BRuntimeConfig, isOfficialQwen35_4BRuntimeConfig,
matchesOfficialRuntimeContract, modelNameForConfig, and
validateModelConfigMatch. Describe each function’s purpose, configuration input,
return value where applicable, and document that validateModelConfigMatch throws
std::invalid_argument for unsupported runtime configurations, missing
embeddings, incompatible embedding dtype, or invalid embedding size/shape.

---

Nitpick comments:
In `@examples/qwen3_5/test_validators.py`:
- Around line 158-214: Extend
test_kai_recipe_contract_rejects_runtime_incompatible_hints to add negative
cases for validate_kai_recipe_contract’s missing/unexpected-linear coverage
validation and tied-embedding replace/rename enforcement. Mutate the loaded
quantization or runtime configuration minimally to trigger each branch, assert
AssertionError, and keep the existing hint and linear_impl_type checks
unchanged.

In `@examples/qwen3_5/validate_converted_model.py`:
- Around line 215-239: Centralize the uint32 boundary as a module-level
UINT32_LIMIT in examples/qwen3_5/validate_converted_model.py:215-239 and use it
for crosses_uint32_offset instead of the inline 1 << 32 literal. In
examples/qwen3_5/test_validators.py:32-34, import UINT32_LIMIT from
validate_converted_model and remove the local duplicate definition.
🪄 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: 99584525-da62-43e6-916c-d6fabe8f80ac

📥 Commits

Reviewing files that changed from the base of the PR and between bd7bcf7 and b27f801.

📒 Files selected for processing (14)
  • README.md
  • examples/qwen3_5/README.md
  • examples/qwen3_5/config_0.8B_w4a32_kai.json
  • examples/qwen3_5/config_4B_w4a32_kai.json
  • examples/qwen3_5/main.cpp
  • examples/qwen3_5/quant_cfg_4B_w4a32_kai.json
  • examples/qwen3_5/test_validators.py
  • examples/qwen3_5/validate_checkpoint.py
  • examples/qwen3_5/validate_converted_model.py
  • mllm/models/qwen3_5/configuration_qwen3_5.hpp
  • mllm/models/qwen3_5/modeling_qwen3_5.hpp
  • pymllm/mobile/tests/test_convertor.py
  • tests/cpu/CMakeLists.txt
  • tests/cpu/Qwen35ConfigTest.cpp

Comment thread examples/qwen3_5/validate_checkpoint.py
Comment thread mllm/models/qwen3_5/configuration_qwen3_5.hpp
- validate_kai_recipe_contract now rejects a missing or non-object runtime
  text_config with a descriptive AssertionError instead of raising KeyError
- document the exported Qwen3.5 runtime-contract helpers in
  configuration_qwen3_5.hpp, including validateModelConfigMatch's
  std::invalid_argument cases
- centralize the unsigned 32-bit offset boundary as UINT32_LIMIT in
  validate_converted_model.py and import it in the tests
- add negative coverage for missing/unexpected KAI Linear coverage, invalid
  tied-embedding entries, and malformed runtime text_config

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

Caution

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

⚠️ Outside diff range comments (1)
mllm/models/qwen3_5/configuration_qwen3_5.hpp (1)

198-213: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Validate every special-token ID in the runtime contract.

The contract description includes special-token IDs, but only eos_token_id and im_end_token_id are checked. end_of_text_token_id, im_start_token_id, and both thinking IDs retain defaults because they are not parsed or validated, so an incompatible config can be accepted. Load and compare all supported token IDs.

🤖 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/configuration_qwen3_5.hpp` around lines 198 - 213, Extend
hasOfficialCommonRuntimeContract to load and validate every supported
special-token ID, including end_of_text_token_id, im_start_token_id, and both
thinking IDs, alongside the existing eos_token_id and im_end_token_id checks.
Ensure these IDs are parsed from the configuration before comparison so
incompatible values cannot pass validation.
🧹 Nitpick comments (1)
examples/qwen3_5/test_validators.py (1)

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

Add docstrings to the new test methods.

Each new public test function needs a brief docstring describing its rejected contract case.

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

Also applies to: 237-260, 262-299, 301-321

🤖 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/test_validators.py` around lines 216 - 235, Add brief
docstrings to each newly added public test method, including
test_kai_recipe_contract_rejects_missing_linear_coverage and the methods in the
referenced ranges, describing the rejected contract case each test verifies.
Keep the existing test logic unchanged.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@mllm/models/qwen3_5/configuration_qwen3_5.hpp`:
- Around line 198-213: Extend hasOfficialCommonRuntimeContract to load and
validate every supported special-token ID, including end_of_text_token_id,
im_start_token_id, and both thinking IDs, alongside the existing eos_token_id
and im_end_token_id checks. Ensure these IDs are parsed from the configuration
before comparison so incompatible values cannot pass validation.

---

Nitpick comments:
In `@examples/qwen3_5/test_validators.py`:
- Around line 216-235: Add brief docstrings to each newly added public test
method, including test_kai_recipe_contract_rejects_missing_linear_coverage and
the methods in the referenced ranges, describing the rejected contract case each
test verifies. Keep the existing test logic unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8173d3c3-74d9-4433-b2f5-009f7a28f029

📥 Commits

Reviewing files that changed from the base of the PR and between b27f801 and 390f1f1.

📒 Files selected for processing (4)
  • examples/qwen3_5/test_validators.py
  • examples/qwen3_5/validate_checkpoint.py
  • examples/qwen3_5/validate_converted_model.py
  • mllm/models/qwen3_5/configuration_qwen3_5.hpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/qwen3_5/validate_converted_model.py

@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 2c889d9 into UbiquitousLearning:main Jul 31, 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