Skip to content

Commit f598969

Browse files
committed
feat(cpu): add Ling-3.0-tiny mobile support
1 parent 4978281 commit f598969

25 files changed

Lines changed: 2452 additions & 3 deletions

README-ZH.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ mllm 框架可以与主流社区框架的模型检查点无缝集成。通过 ml
107107
| [Qwen3-4B](https://github.com/QwenLM/Qwen3) | [✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/Qwen3-4B-w4a8-i8mm-kai) | | |
108108
| Qwen3.5-2B | | ✔️ W4A16 / W8A8 | |
109109
| Qwen3.5-4B | | ✔️ W4A16 / W8A8 | |
110+
| [Ling-3.0-tiny](https://huggingface.co/inclusionAI/Ling-3.0-tiny) | [✔️ W4A32,ARM64 macOS/Android](./examples/ling3/README.md) | | |
110111
| Qwen3-VL-2B-Instruct | | ✔️ W4A16 / W8A8 | |
111112
| Qwen3-VL-4B-Instruct | | ✔️ W4A16 / W8A8 | |
112113
| [DeepSeek-OCR](https://github.com/deepseek-ai/DeepSeek-OCR) | [✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/DeepSeek-OCR-w4a8-i8mm-kai) | | |

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ The mllm framework integrates seamlessly with popular community frameworks' chec
106106
| [Qwen3-4B](https://github.com/QwenLM/Qwen3) | [✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/Qwen3-4B-w4a8-i8mm-kai) | | |
107107
| [Qwen3.5-0.8B](https://huggingface.co/Qwen/Qwen3.5-0.8B) | [✔️ w4a8](./examples/qwen3_5/README.md) | | |
108108
| [Qwen3.5-4B](https://huggingface.co/Qwen/Qwen3.5-4B) | [✔️ w4a8](./examples/qwen3_5/README.md) | | |
109+
| [Ling-3.0-tiny](https://huggingface.co/inclusionAI/Ling-3.0-tiny) | [✔️ W4A32, ARM64 macOS/Android](./examples/ling3/README.md) | | |
109110
| [DeepSeek-OCR](https://github.com/deepseek-ai/DeepSeek-OCR) | [✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/DeepSeek-OCR-w4a8-i8mm-kai) | | |
110111
| [SmolLM3](https://huggingface.co/blog/smollm3)| [✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/SmolLM3-3B-w4a8-i8mm-kai) | | |
111112
| [Qwen2-VL-2B-Instruct](https://qwenlm.github.io/zh/blog/qwen2-vl/)|[✔️ w4a8](https://www.modelscope.cn/models/mllmTeam/Qwen2-VL-2B-Instruct-w4a32kai) || |
357 KB
Loading

examples/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ add_subdirectory(minicpm_o)
77
add_subdirectory(minicpm4)
88
add_subdirectory(qwen3)
99
add_subdirectory(qwen3_5)
10+
add_subdirectory(ling3)
1011
add_subdirectory(qwen3_service)
1112
add_subdirectory(qwen3_moe)
1213
add_subdirectory(deepseek_ocr)

examples/ling3/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
add_executable(mllm-ling3-runner main.cpp)
2+
target_link_libraries(mllm-ling3-runner PRIVATE MllmRT MllmCPUBackend)
3+
target_include_directories(mllm-ling3-runner PRIVATE ${MLLM_INCLUDE_DIR})

examples/ling3/README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Ling-3.0-tiny mobile CPU runner
2+
3+
This runner targets the pinned official checkpoint
4+
`inclusionAI/Ling-3.0-tiny@a2ee06c0f2de5b171701aee7f73f70a1da75483b`.
5+
The supported deployment envelope is batch 1, a 2048-token cache, float32
6+
recurrent state, and KAI W4A32 linear weights on ARM64 macOS and Android.
7+
Build the ARM CPU backend without `-ffast-math`: Ling-3's recurrent gates are
8+
not compatible with relaxed IEEE semantics. For Android, the validated ISA
9+
flags are `-march=armv8.2-a+fp16+fp16fml+dotprod+i8mm`; do not append
10+
`-ffast-math`.
11+
12+
Validate the source checkpoint before conversion:
13+
14+
```bash
15+
python3 validate_checkpoint.py /path/to/Ling-3.0-tiny \
16+
--observed-revision a2ee06c0f2de5b171701aee7f73f70a1da75483b
17+
```
18+
19+
Convert with the repository V2 converter and
20+
`quant_cfg_tiny_w4a32_kai.json`, using model name `Ling-3.0-tiny`, then seal
21+
the output descriptor table:
22+
23+
```bash
24+
python3 validate_converted_model.py /path/to/Ling-3.0-tiny.mllm \
25+
/path/to/Ling-3.0-tiny
26+
```
27+
28+
Run one deterministic smoke request:
29+
30+
```bash
31+
./mllm-ling3-runner \
32+
--model_path /path/to/Ling-3.0-tiny.mllm \
33+
--tokenizer_path /path/to/Ling-3.0-tiny/tokenizer.json \
34+
--config_path config_tiny_w4a32_kai.json \
35+
--prompt '你好,请用一句话介绍你自己。' \
36+
--max_new_tokens 8 --print_token_ids
37+
```
38+
39+
The runner emits `LING3_RUN_START`, generated token IDs, and
40+
`LING3_RUN_OK`. A successful build or tokenizer-only test is not a full-model
41+
runtime result; device evidence must retain the converted model SHA256 and
42+
the runner/library identities together.
43+
44+
For a longer deterministic correctness demo, use the checked-in prompt and a
45+
64-token generation limit:
46+
47+
```bash
48+
./mllm-ling3-runner \
49+
--model_path /path/to/Ling-3.0-tiny.mllm \
50+
--tokenizer_path /path/to/Ling-3.0-tiny/tokenizer.json \
51+
--config_path config_tiny_w4a32_kai.json \
52+
--prompt_file demo_prompt_v1.txt --disable_thinking \
53+
--max_new_tokens 64 --print_token_ids
54+
```
55+
56+
The expected completion marker is
57+
`LING3_RUN_OK prompt_tokens=49 generated_tokens=64`. This is a generation
58+
correctness demo, not a perplexity, model-quality, or performance benchmark.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"architectures": ["BailingMoeV3ForCausalLM"],
3+
"model_type": "bailing_hybrid",
4+
"hidden_size": 1536,
5+
"intermediate_size": 4608,
6+
"num_hidden_layers": 24,
7+
"num_attention_heads": 16,
8+
"num_key_value_heads": 16,
9+
"head_dim": 128,
10+
"vocab_size": 157184,
11+
"max_position_embeddings": 131072,
12+
"rms_norm_eps": 1e-06,
13+
"rope_theta": 6000000,
14+
"hidden_act": "silu",
15+
"tie_word_embeddings": false,
16+
"use_qkv_bias": false,
17+
"layer_group_size": 4,
18+
"short_conv_kernel_size": 4,
19+
"no_kda_lora": true,
20+
"kda_safe_gate": true,
21+
"kda_lower_bound": -5,
22+
"q_lora_rank": 256,
23+
"kv_lora_rank": 512,
24+
"qk_rope_head_dim": 64,
25+
"qk_nope_head_dim": 128,
26+
"qk_head_dim": 192,
27+
"v_head_dim": 128,
28+
"rope_interleave": true,
29+
"gated_attention_proj_granularity_type": "head_wise",
30+
"num_experts": 128,
31+
"num_shared_experts": 1,
32+
"num_experts_per_tok": 8,
33+
"n_group": 8,
34+
"topk_group": 4,
35+
"moe_intermediate_size": 512,
36+
"moe_shared_expert_intermediate_size": 512,
37+
"first_k_dense_replace": 1,
38+
"routed_scaling_factor": 2.5,
39+
"scoring_func": "sigmoid",
40+
"topk_method": "noaux_tc",
41+
"moe_router_enable_expert_bias": true,
42+
"pad_token_id": 156892,
43+
"eos_token_id": 156895,
44+
"max_cache_length": 2048,
45+
"linear_impl_type": "KaiLinear_f32_qai8dxp_qsi4c32p_mxk_nxk_qai8dxp1x8_qsi4c32p8x8_1x8x32"
46+
}

examples/ling3/demo_prompt_v1.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
请用中文详细介绍 Ling-3.0-tiny 的混合注意力架构,并解释 KDA、MLA 和 MoE 各自的作用。

examples/ling3/main.cpp

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// Copyright (c) MLLM Team.
2+
// Licensed under the MIT License.
3+
4+
#include <fmt/core.h>
5+
6+
#include <mllm/engine/Context.hpp>
7+
#include <mllm/mllm.hpp>
8+
#include <mllm/models/ling3/modeling_ling3.hpp>
9+
#include <mllm/models/ling3/tokenization_ling3.hpp>
10+
#include <mllm/utils/AnyValue.hpp>
11+
12+
#include <cstdio>
13+
#include <fstream>
14+
#include <iostream>
15+
#include <stdexcept>
16+
#include <string>
17+
18+
using mllm::Argparse;
19+
20+
namespace {
21+
22+
std::string readPromptFile(const std::string& path) {
23+
std::ifstream stream(path, std::ios::binary);
24+
if (!stream) { throw std::invalid_argument("unable to read prompt_file: " + path); }
25+
std::string text{std::istreambuf_iterator<char>(stream), std::istreambuf_iterator<char>()};
26+
while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) { text.pop_back(); }
27+
if (text.empty()) { throw std::invalid_argument("prompt_file must not be empty: " + path); }
28+
return text;
29+
}
30+
31+
} // namespace
32+
33+
MLLM_MAIN({
34+
auto engine_args = mllm::engineArgAttach();
35+
auto& help = Argparse::add<bool>("-h|--help").help("Show help message");
36+
auto& model_path = Argparse::add<std::string>("-m|--model_path").help("MLLM V2 model path").required(true);
37+
auto& tokenizer_path = Argparse::add<std::string>("-t|--tokenizer_path").help("Official tokenizer.json").required(true);
38+
auto& config_path = Argparse::add<std::string>("-c|--config_path").help("Ling-3 mobile runtime config").required(true);
39+
auto& prompt = Argparse::add<std::string>("-p|--prompt").help("Run one prompt non-interactively").required(false);
40+
auto& prompt_file = Argparse::add<std::string>("--prompt_file").help("Read a prompt from a UTF-8 file").required(false);
41+
auto& max_new_tokens =
42+
Argparse::add<int>("-g|--max_new_tokens").help("Maximum generated tokens (default: 8)").required(false);
43+
auto& min_new_tokens = Argparse::add<int>("--min_new_tokens").help("Suppress EOS until this many tokens").required(false);
44+
auto& disable_thinking =
45+
Argparse::add<bool>("--disable_thinking").help("Use the official thinking-off chat template").required(false);
46+
auto& print_token_ids = Argparse::add<bool>("--print_token_ids").help("Print generated token IDs to stderr").required(false);
47+
48+
for (int index = 1; index < argc; ++index) {
49+
if (std::string(argv[index]) == "-h" || std::string(argv[index]) == "--help") {
50+
Argparse::printHelp();
51+
return 0;
52+
}
53+
}
54+
Argparse::parse(argc, argv);
55+
mllm::configEngineWithArgs(engine_args);
56+
(void)help;
57+
58+
const auto config = mllm::models::ling3::Ling3Config(config_path.get());
59+
int generation_limit = max_new_tokens.isSet() ? max_new_tokens.get() : 8;
60+
int minimum_generation = min_new_tokens.isSet() ? min_new_tokens.get() : 0;
61+
if (generation_limit <= 0 || generation_limit > config.max_cache_length || minimum_generation < 0
62+
|| minimum_generation > generation_limit) {
63+
throw std::invalid_argument("generation lengths must satisfy 0 <= min_new_tokens <= max_new_tokens <= max_cache_length");
64+
}
65+
if (prompt.isSet() && prompt_file.isSet()) { throw std::invalid_argument("prompt and prompt_file are mutually exclusive"); }
66+
67+
std::string configured_prompt;
68+
if (prompt_file.isSet()) {
69+
configured_prompt = readPromptFile(prompt_file.get());
70+
} else if (prompt.isSet()) {
71+
configured_prompt = prompt.get();
72+
}
73+
74+
const auto parameters = mllm::load(model_path.get(), mllm::ModelFileVersion::kV2);
75+
mllm::models::ling3::validateLing3ModelConfigMatch(config, parameters);
76+
auto tokenizer = mllm::models::ling3::Ling3Tokenizer(tokenizer_path.get());
77+
auto model = mllm::models::ling3::Ling3ForCausalLM(config);
78+
model.load(parameters);
79+
fmt::print("Ling-3.0-tiny: {} layers ({} MLA + {} KDA), CPU threads={}\n", config.num_hidden_layers,
80+
config.numFullAttentionLayers(), config.numKDALayers(), mllm::Context::instance().getCpuOpThreads());
81+
82+
int exit_code = 0;
83+
while (true) {
84+
std::string prompt_text = configured_prompt;
85+
if (!prompt.isSet() && !prompt_file.isSet()) {
86+
fmt::print("Prompt text (or 'exit/quit'): ");
87+
if (!std::getline(std::cin, prompt_text) || prompt_text == "exit" || prompt_text == "quit") { break; }
88+
}
89+
if (prompt_text.empty()) {
90+
if (prompt.isSet() || prompt_file.isSet()) { throw std::invalid_argument("prompt must not be empty"); }
91+
continue;
92+
}
93+
try {
94+
auto input = tokenizer.convertMessage({.prompt = prompt_text,
95+
.system_prompt = "",
96+
.enable_thinking = !(disable_thinking.isSet() && disable_thinking.get())});
97+
const int prompt_tokens = input.at("sequence").shape()[1];
98+
if (prompt_tokens + generation_limit - 1 > config.max_cache_length) {
99+
throw std::invalid_argument("prompt plus generation exceeds max_cache_length");
100+
}
101+
model.resetState();
102+
fmt::print("LING3_RUN_START prompt_tokens={} max_new_tokens={} min_new_tokens={}\nResponse: ", prompt_tokens,
103+
generation_limit, minimum_generation);
104+
int generated_tokens = 0;
105+
for (const auto& step : model.chat(input, {{"max_length", mllm::AnyValue(generation_limit)},
106+
{"min_new_tokens", mllm::AnyValue(minimum_generation)},
107+
{"do_sample", mllm::AnyValue(false)}})) {
108+
if (print_token_ids.isSet() && print_token_ids.get()) { fmt::print(stderr, "LING3_TOKEN_ID:{}\n", step.cur_token_id); }
109+
fmt::print("{}", tokenizer.detokenizeBytes(step.cur_token_id));
110+
std::fflush(stdout);
111+
++generated_tokens;
112+
}
113+
fmt::print("\nLING3_RUN_OK prompt_tokens={} generated_tokens={}\n", prompt_tokens, generated_tokens);
114+
} catch (const std::exception& error) {
115+
fmt::print(stderr, "LING3_RUN_ERROR:{}\n", error.what());
116+
exit_code = 1;
117+
}
118+
if (prompt.isSet() || prompt_file.isSet()) { break; }
119+
}
120+
121+
model.perfSummary();
122+
mllm::memoryReport();
123+
return exit_code;
124+
})
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"^model\\.layers\\.\\d+\\.attention\\.(q_proj|k_proj|v_proj|f_proj)\\.weight$": {
3+
"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}
4+
},
5+
"^model\\.layers\\.(?:[0-2]|[4-6]|8|9|10|12|13|14|16|17|18|20|21|22)\\.attention\\.g_proj\\.weight$": {
6+
"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}
7+
},
8+
"^model\\.layers\\.(?:3|7|11|15|19|23)\\.attention\\.g_proj\\.weight$": {
9+
"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}
10+
},
11+
"^model\\.layers\\.\\d+\\.attention\\.b_proj\\.weight$": {
12+
"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}
13+
},
14+
"^model\\.layers\\.\\d+\\.attention\\.o_proj\\.weight$": {
15+
"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}
16+
},
17+
"^model\\.layers\\.\\d+\\.attention\\.q_a_proj\\.weight$": {
18+
"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}
19+
},
20+
"^model\\.layers\\.\\d+\\.attention\\.q_b_proj\\.weight$": {
21+
"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}
22+
},
23+
"^model\\.layers\\.\\d+\\.attention\\.kv_a_proj_with_mqa\\.weight$": {
24+
"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}
25+
},
26+
"^model\\.layers\\.\\d+\\.attention\\.kv_b_proj\\.weight$": {
27+
"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}
28+
},
29+
"^model\\.layers\\.\\d+\\.attention\\.dense\\.weight$": {
30+
"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}
31+
},
32+
"^model\\.layers\\.0\\.mlp\\.(gate_proj|up_proj)\\.weight$": {
33+
"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}
34+
},
35+
"^model\\.layers\\.0\\.mlp\\.down_proj\\.weight$": {
36+
"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}
37+
},
38+
"^model\\.layers\\.(?:[1-9]|1\\d|2[0-3])\\.mlp\\.(?:experts\\.\\d+|shared_experts)\\.(gate_proj|up_proj)\\.weight$": {
39+
"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}
40+
},
41+
"^model\\.layers\\.(?:[1-9]|1\\d|2[0-3])\\.mlp\\.(?:experts\\.\\d+|shared_experts)\\.down_proj\\.weight$": {
42+
"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}
43+
},
44+
"^lm_head\\.weight$": {
45+
"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}
46+
}
47+
}

0 commit comments

Comments
 (0)