Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README-ZH.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ mllm 框架可以与主流社区框架的模型检查点无缝集成。通过 ml
| [Qwen3-4B](https://github.com/QwenLM/Qwen3) | [✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/Qwen3-4B-w4a8-i8mm-kai) | | |
| Qwen3.5-2B | | ✔️ W4A16 / W8A8 | |
| Qwen3.5-4B | | ✔️ W4A16 / W8A8 | |
| [Ling-3.0-tiny](https://huggingface.co/inclusionAI/Ling-3.0-tiny) | [✔️ W4A32,ARM64 macOS/Android](./examples/ling3/README.md) | | |
| Qwen3-VL-2B-Instruct | | ✔️ W4A16 / W8A8 | |
| Qwen3-VL-4B-Instruct | | ✔️ W4A16 / W8A8 | |
| [DeepSeek-OCR](https://github.com/deepseek-ai/DeepSeek-OCR) | [✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/DeepSeek-OCR-w4a8-i8mm-kai) | | |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ The mllm framework integrates seamlessly with popular community frameworks' chec
| [Qwen3.5-4B](https://huggingface.co/Qwen/Qwen3.5-4B) | [✔️ w4a8](./examples/qwen3_5/README.md) | | |
| [MiniCPM5-1B](https://huggingface.co/openbmb/MiniCPM5-1B) | [✔️ w4a8](./examples/minicpm5/README.md) | | |
| [LFM2.5-2.6B](https://huggingface.co/LiquidAI/LFM2.5-2.6B) | [✔️ w4a8](./examples/lfm2/README.md) | | |
| [Ling-3.0-tiny](https://huggingface.co/inclusionAI/Ling-3.0-tiny) | [✔️ W4A32, ARM64 macOS/Android](./examples/ling3/README.md) | | |
| [DeepSeek-OCR](https://github.com/deepseek-ai/DeepSeek-OCR) | [✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/DeepSeek-OCR-w4a8-i8mm-kai) | | |
| [SmolLM3](https://huggingface.co/blog/smollm3)| [✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/SmolLM3-3B-w4a8-i8mm-kai) | | |
| [Qwen2-VL-2B-Instruct](https://qwenlm.github.io/zh/blog/qwen2-vl/)|[✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/Qwen2-VL-2B-Instruct-w4a32kai) || |
Expand Down
Binary file added bench_assets/ling3_tiny_architecture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ add_subdirectory(minicpm5)
add_subdirectory(qwen3)
add_subdirectory(qwen3_5)
add_subdirectory(lfm2)
add_subdirectory(ling3)
add_subdirectory(qwen3_service)
add_subdirectory(qwen3_moe)
add_subdirectory(deepseek_ocr)
Expand Down
3 changes: 3 additions & 0 deletions examples/ling3/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
add_executable(mllm-ling3-runner main.cpp)
target_link_libraries(mllm-ling3-runner PRIVATE MllmRT MllmCPUBackend)
target_include_directories(mllm-ling3-runner PRIVATE ${MLLM_INCLUDE_DIR})
70 changes: 70 additions & 0 deletions examples/ling3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Ling-3.0-tiny mobile CPU runner

This runner targets the pinned official checkpoint
`inclusionAI/Ling-3.0-tiny@a2ee06c0f2de5b171701aee7f73f70a1da75483b`.
The supported deployment envelope is batch 1, a 2048-token cache, float32
recurrent state, and KAI W4A32 linear weights on ARM64 macOS and Android.
Build the ARM CPU backend without `-ffast-math`: Ling-3's recurrent gates are
not compatible with relaxed IEEE semantics. For Android, the validated ISA
flags are `-march=armv8.2-a+fp16+fp16fml+dotprod+i8mm`; do not append
`-ffast-math`.

Validate the source checkpoint before conversion:

```bash
python3 validate_checkpoint.py /path/to/Ling-3.0-tiny \
--observed-revision a2ee06c0f2de5b171701aee7f73f70a1da75483b
```

Convert with the repository V2 converter and
`quant_cfg_tiny_w4a32_kai.json`, using model name `Ling-3.0-tiny`, then seal
the output descriptor table:

```bash
python3 validate_converted_model.py /path/to/Ling-3.0-tiny.mllm \
/path/to/Ling-3.0-tiny
```

Run one deterministic smoke request:

```bash
./mllm-ling3-runner \
--model_path /path/to/Ling-3.0-tiny.mllm \
--tokenizer_path /path/to/Ling-3.0-tiny/tokenizer.json \
--config_path config_tiny_w4a32_kai.json \
--prompt '你好,请用一句话介绍你自己。' \
--max_new_tokens 8 --print_token_ids
Comment on lines +12 to +36

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

State the required working directory for these commands.

The commands reference validate_checkpoint.py, validate_converted_model.py, and config_tiny_w4a32_kai.json with relative paths. They fail when users run them from the repository root.

Add an explicit cd examples/ling3 step, or make all script, configuration, and runner paths repository-relative.

🤖 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/ling3/README.md` around lines 12 - 36, Update the Ling-3 README
command sequence to establish the required working directory before using
relative paths: add an explicit cd examples/ling3 step before the validation,
conversion, and smoke-run commands, or convert every referenced script,
configuration, and runner path to repository-relative paths.

```

The runner emits `LING3_RUN_START`, generated token IDs, and
`LING3_RUN_OK`. A successful build or tokenizer-only test is not a full-model
runtime result; device evidence must retain the converted model SHA256 and
the runner/library identities together.

For a longer correctness demo, pass the prompt directly and use a 64-token
generation limit:

```bash
./mllm-ling3-runner \
--model_path /path/to/Ling-3.0-tiny.mllm \
--tokenizer_path /path/to/Ling-3.0-tiny/tokenizer.json \
--config_path config_tiny_w4a32_kai.json \
--prompt '请用中文详细介绍 Ling-3.0-tiny 的混合注意力架构,并解释 KDA、MLA 和 MoE 各自的作用。' --disable_thinking \
--max_new_tokens 64 --print_token_ids
```

The expected completion marker is
`LING3_RUN_OK prompt_tokens=49 generated_tokens=64`. This is a generation
correctness demo, not a perplexity, model-quality, or performance benchmark.

## Retained tests

The Ling-specific coverage follows the repository test layout (`ctest` labels
in parentheses):

| Layer | Target | Notes |
| --- | --- | --- |
| KDA kernel | `Mllm-Test-CPUKernel --gtest_filter='KimiDeltaAttentionKernelTest.*'` (`cpu-kernel`) | scalar-reference oracle, bitwise prefill/decode and serial/parallel checks |
| Public `nn::KimiDeltaAttention` | `Mllm-Test-Nn-KimiDeltaAttention` (`KimiDeltaAttentionFocused`) | eager reference, in-place state, chunked equivalence, trace + serialization |
| Causal convolution | upstream `Mllm-Test-Nn-CausalDepthwiseConv1D` and the `CausalDepthwiseConv*KernelTest` suites | Ling registers the upstream operation with the current-first order |
| Model | `Mllm-Test-Ling3-Config`, `Mllm-Test-Ling3-RoPE`, `Mllm-Test-Ling3-Tokenizer` (`ling3`) | set `MLLM_LING3_EXAMPLE_DIR` on a device; the tokenizer test skips unless `LING3_OFFICIAL_TOKENIZER` points at the official `tokenizer.json` |
46 changes: 46 additions & 0 deletions examples/ling3/config_tiny_w4a32_kai.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"architectures": ["BailingMoeV3ForCausalLM"],
"model_type": "bailing_hybrid",
"hidden_size": 1536,
"intermediate_size": 4608,
"num_hidden_layers": 24,
"num_attention_heads": 16,
"num_key_value_heads": 16,
"head_dim": 128,
"vocab_size": 157184,
"max_position_embeddings": 131072,
"rms_norm_eps": 1e-06,
"rope_theta": 6000000,
"hidden_act": "silu",
"tie_word_embeddings": false,
"use_qkv_bias": false,
"layer_group_size": 4,
"short_conv_kernel_size": 4,
"no_kda_lora": true,
"kda_safe_gate": true,
"kda_lower_bound": -5,
"q_lora_rank": 256,
"kv_lora_rank": 512,
"qk_rope_head_dim": 64,
"qk_nope_head_dim": 128,
"qk_head_dim": 192,
"v_head_dim": 128,
"rope_interleave": true,
"gated_attention_proj_granularity_type": "head_wise",
"num_experts": 128,
"num_shared_experts": 1,
"num_experts_per_tok": 8,
"n_group": 8,
"topk_group": 4,
"moe_intermediate_size": 512,
"moe_shared_expert_intermediate_size": 512,
"first_k_dense_replace": 1,
"routed_scaling_factor": 2.5,
"scoring_func": "sigmoid",
"topk_method": "noaux_tc",
"moe_router_enable_expert_bias": true,
"pad_token_id": 156892,
"eos_token_id": 156895,
"max_cache_length": 2048,
"linear_impl_type": "KaiLinear_f32_qai8dxp_qsi4c32p_mxk_nxk_qai8dxp1x8_qsi4c32p8x8_1x8x32"
}
124 changes: 124 additions & 0 deletions examples/ling3/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// Copyright (c) MLLM Team.
// Licensed under the MIT License.

#include <fmt/core.h>

#include <mllm/engine/Context.hpp>
#include <mllm/mllm.hpp>
#include <mllm/models/ling3/modeling_ling3.hpp>
#include <mllm/models/ling3/tokenization_ling3.hpp>
#include <mllm/utils/AnyValue.hpp>

#include <cstdio>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>

using mllm::Argparse;

namespace {

std::string readPromptFile(const std::string& path) {
std::ifstream stream(path, std::ios::binary);
if (!stream) { throw std::invalid_argument("unable to read prompt_file: " + path); }
std::string text{std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>()};
while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) { text.pop_back(); }
if (text.empty()) { throw std::invalid_argument("prompt_file must not be empty: " + path); }
return text;
}

} // namespace

MLLM_MAIN({
auto engine_args = mllm::engineArgAttach();
auto& help = Argparse::add<bool>("-h|--help").help("Show help message");
auto& model_path = Argparse::add<std::string>("-m|--model_path").help("MLLM V2 model path").required(true);
auto& tokenizer_path = Argparse::add<std::string>("-t|--tokenizer_path").help("Official tokenizer.json").required(true);
auto& config_path = Argparse::add<std::string>("-c|--config_path").help("Ling-3 mobile runtime config").required(true);
auto& prompt = Argparse::add<std::string>("-p|--prompt").help("Run one prompt non-interactively").required(false);
auto& prompt_file = Argparse::add<std::string>("--prompt_file").help("Read a prompt from a UTF-8 file").required(false);
auto& max_new_tokens =
Argparse::add<int>("-g|--max_new_tokens").help("Maximum generated tokens (default: 8)").required(false);
auto& min_new_tokens = Argparse::add<int>("--min_new_tokens").help("Suppress EOS until this many tokens").required(false);
auto& disable_thinking =
Argparse::add<bool>("--disable_thinking").help("Use the official thinking-off chat template").required(false);
auto& print_token_ids = Argparse::add<bool>("--print_token_ids").help("Print generated token IDs to stderr").required(false);

for (int index = 1; index < argc; ++index) {
if (std::string(argv[index]) == "-h" || std::string(argv[index]) == "--help") {
Argparse::printHelp();
return 0;
}
}
Argparse::parse(argc, argv);
mllm::configEngineWithArgs(engine_args);
(void)help;

const auto config = mllm::models::ling3::Ling3Config(config_path.get());
int generation_limit = max_new_tokens.isSet() ? max_new_tokens.get() : 8;
int minimum_generation = min_new_tokens.isSet() ? min_new_tokens.get() : 0;
if (generation_limit <= 0 || generation_limit > config.max_cache_length || minimum_generation < 0
|| minimum_generation > generation_limit) {
throw std::invalid_argument("generation lengths must satisfy 0 <= min_new_tokens <= max_new_tokens <= max_cache_length");
}
if (prompt.isSet() && prompt_file.isSet()) { throw std::invalid_argument("prompt and prompt_file are mutually exclusive"); }

std::string configured_prompt;
if (prompt_file.isSet()) {
configured_prompt = readPromptFile(prompt_file.get());
} else if (prompt.isSet()) {
configured_prompt = prompt.get();
}

const auto parameters = mllm::load(model_path.get(), mllm::ModelFileVersion::kV2);
mllm::models::ling3::validateLing3ModelConfigMatch(config, parameters);
auto tokenizer = mllm::models::ling3::Ling3Tokenizer(tokenizer_path.get());
auto model = mllm::models::ling3::Ling3ForCausalLM(config);
model.load(parameters);
fmt::print("Ling-3.0-tiny: {} layers ({} MLA + {} KDA), CPU threads={}\n", config.num_hidden_layers,
config.numFullAttentionLayers(), config.numKDALayers(), mllm::Context::instance().getCpuOpThreads());

int exit_code = 0;
while (true) {
std::string prompt_text = configured_prompt;
if (!prompt.isSet() && !prompt_file.isSet()) {
fmt::print("Prompt text (or 'exit/quit'): ");
if (!std::getline(std::cin, prompt_text) || prompt_text == "exit" || prompt_text == "quit") { break; }
}
if (prompt_text.empty()) {
if (prompt.isSet() || prompt_file.isSet()) { throw std::invalid_argument("prompt must not be empty"); }
continue;
}
try {
auto input = tokenizer.convertMessage({.prompt = prompt_text,
.system_prompt = "",
.enable_thinking = !(disable_thinking.isSet() && disable_thinking.get())});
const int prompt_tokens = input.at("sequence").shape()[1];
if (prompt_tokens + generation_limit - 1 > config.max_cache_length) {
throw std::invalid_argument("prompt plus generation exceeds max_cache_length");
}
model.resetState();
fmt::print("LING3_RUN_START prompt_tokens={} max_new_tokens={} min_new_tokens={}\nResponse: ", prompt_tokens,
generation_limit, minimum_generation);
int generated_tokens = 0;
for (const auto& step : model.chat(input, {{"max_length", mllm::AnyValue(generation_limit)},
{"min_new_tokens", mllm::AnyValue(minimum_generation)},
{"do_sample", mllm::AnyValue(false)}})) {
if (print_token_ids.isSet() && print_token_ids.get()) { fmt::print(stderr, "LING3_TOKEN_ID:{}\n", step.cur_token_id); }
fmt::print("{}", tokenizer.detokenizeBytes(step.cur_token_id));
std::fflush(stdout);
++generated_tokens;
}
fmt::print("\nLING3_RUN_OK prompt_tokens={} generated_tokens={}\n", prompt_tokens, generated_tokens);
} catch (const std::exception& error) {
fmt::print(stderr, "LING3_RUN_ERROR:{}\n", error.what());
exit_code = 1;
}
if (prompt.isSet() || prompt_file.isSet()) { break; }
}

model.perfSummary();
mllm::memoryReport();
return exit_code;
})
47 changes: 47 additions & 0 deletions examples/ling3/quant_cfg_tiny_w4a32_kai.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
{
"^model\\.layers\\.\\d+\\.attention\\.(q_proj|k_proj|v_proj|f_proj)\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [2048, 1536], "replace": true}
},
"^model\\.layers\\.(?:[0-2]|[4-6]|8|9|10|12|13|14|16|17|18|20|21|22)\\.attention\\.g_proj\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [2048, 1536], "replace": true}
},
"^model\\.layers\\.(?:3|7|11|15|19|23)\\.attention\\.g_proj\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [16, 1536], "replace": true}
},
"^model\\.layers\\.\\d+\\.attention\\.b_proj\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [16, 1536], "replace": true}
},
"^model\\.layers\\.\\d+\\.attention\\.o_proj\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [1536, 2048], "replace": true}
},
"^model\\.layers\\.\\d+\\.attention\\.q_a_proj\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [256, 1536], "replace": true}
},
"^model\\.layers\\.\\d+\\.attention\\.q_b_proj\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [3072, 256], "replace": true}
},
"^model\\.layers\\.\\d+\\.attention\\.kv_a_proj_with_mqa\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [576, 1536], "replace": true}
},
"^model\\.layers\\.\\d+\\.attention\\.kv_b_proj\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [4096, 512], "replace": true}
},
"^model\\.layers\\.\\d+\\.attention\\.dense\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [1536, 2048], "replace": true}
},
"^model\\.layers\\.0\\.mlp\\.(gate_proj|up_proj)\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [4608, 1536], "replace": true}
},
"^model\\.layers\\.0\\.mlp\\.down_proj\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [1536, 4608], "replace": true}
},
"^model\\.layers\\.(?:[1-9]|1\\d|2[0-3])\\.mlp\\.(?:experts\\.\\d+|shared_experts)\\.(gate_proj|up_proj)\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [512, 1536], "replace": true}
},
"^model\\.layers\\.(?:[1-9]|1\\d|2[0-3])\\.mlp\\.(?:experts\\.\\d+|shared_experts)\\.down_proj\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [1536, 512], "replace": true}
},
"^lm_head\\.weight$": {
"hints": {"quant_method": "kai", "kai_matmul_triplet": "f32_qai8dxp_qsi4c32p", "kai_matmul_layout": "mxk_nxk", "kai_matmul_tile_cfg": "qai8dxp1x8_qsi4c32p8x8_1x8x32", "shape": [157184, 1536], "replace": true}
}
}
39 changes: 39 additions & 0 deletions examples/ling3/test_validators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) MLLM Team.
# Licensed under the MIT License.

import json
import unittest
from pathlib import Path

from validate_checkpoint import (
FULL_ATTENTION_LAYERS,
expected_shapes,
validate_config,
validate_recipe,
)


HERE = Path(__file__).resolve().parent


class Ling3ValidatorTests(unittest.TestCase):
def test_expected_checkpoint_inventory_is_exact(self):
shapes = expected_shapes()
self.assertEqual(len(shapes), 9283)
self.assertEqual(
{layer for layer in range(24) if f"model.layers.{layer}.attention.q_a_proj.weight" in shapes},
FULL_ATTENTION_LAYERS,
)

def test_runtime_and_recipe_match_official_contract(self):
runtime = json.loads((HERE / "config_tiny_w4a32_kai.json").read_text())
validate_config(runtime, runtime)
recipe = json.loads((HERE / "quant_cfg_tiny_w4a32_kai.json").read_text())
matched = validate_recipe(expected_shapes(), recipe)
self.assertGreater(len(matched), 9000)
self.assertNotIn("model.word_embeddings.weight", matched)
self.assertNotIn("model.layers.1.mlp.gate.weight", matched)


if __name__ == "__main__":
unittest.main()
Loading
Loading