Implement Qwen text core

This commit is contained in:
Georg Bauer
2026-09-03 20:31:53 +02:00
parent 3773cfda2e
commit c414640050
9 changed files with 2766 additions and 52 deletions

View File

@@ -53,6 +53,7 @@ pub(crate) fn checkpoint_model(path: &Path) -> Option<ModelChoice> {
let model_size_offset = match &magic {
b"DS4RKV01" => 40,
b"DS4GLM01" => 44,
b"DS4QWN01" => return Some(ModelChoice::Qwen38FlashNext),
_ => return None,
};
file.seek(SeekFrom::Start(model_size_offset)).ok()?;
@@ -301,6 +302,115 @@ pub(crate) struct Model {
tokenizer: Tokenizer,
}
enum LoadedModel {
Gguf(Box<Model>),
Qwen(Box<qwen::QwenModel>),
}
impl From<Model> for LoadedModel {
fn from(model: Model) -> Self {
Self::Gguf(Box::new(model))
}
}
#[derive(Clone, Copy)]
enum ModelRef<'a> {
Gguf(&'a Model),
Qwen(&'a qwen::QwenModel),
}
impl LoadedModel {
fn open(settings: &EngineSettings) -> Result<Self, String> {
if settings.model.is_qwen38() {
validate_engine_artifacts(
settings.model,
settings.speculative.dspark,
&settings.artifacts,
)?;
let context = u32::try_from(settings.context_tokens)
.map_err(|_| "Qwen context must be a positive whole number")?;
qwen::QwenModel::open(&settings.artifacts.model, context)
.map(Box::new)
.map(Self::Qwen)
} else {
Model::open(settings).map(Box::new).map(Self::Gguf)
}
}
}
impl ModelRef<'_> {
fn summary(self) -> ModelSummary {
match self {
Self::Gguf(model) => model.summary(),
Self::Qwen(model) => model.summary(),
}
}
fn render_conversation(
self,
system: &str,
messages: &[ChatTurn],
reasoning: ReasoningMode,
) -> Vec<i32> {
match self {
Self::Gguf(model) => model.render_conversation(system, messages, reasoning),
Self::Qwen(model) => model.render_conversation(system, messages, reasoning),
}
}
fn render_history(
self,
system: &str,
messages: &[ChatTurn],
reasoning: ReasoningMode,
) -> Vec<i32> {
match self {
Self::Gguf(model) => model.render_history(system, messages, reasoning),
Self::Qwen(model) => model.render_history(system, messages, reasoning),
}
}
fn render_continuation(
self,
prompt: &str,
reasoning: ReasoningMode,
skip_previous_eos: bool,
) -> Vec<i32> {
match self {
Self::Gguf(model) => model.render_continuation(prompt, reasoning, skip_previous_eos),
Self::Qwen(model) => model.render_continuation(prompt, reasoning, skip_previous_eos),
}
}
fn token_bytes(self, token: i32) -> Option<Vec<u8>> {
match self {
Self::Gguf(model) => model.token_bytes(token),
Self::Qwen(model) => model.token_bytes(token),
}
}
fn is_stop_token_for_reasoning(self, token: i32, reasoning: ReasoningMode) -> bool {
match self {
Self::Gguf(model) => model.is_stop_token_for_reasoning(token, reasoning),
Self::Qwen(model) => model.is_stop_token_for_reasoning(token, reasoning),
}
}
fn is_think_start_token(self, token: i32) -> bool {
match self {
Self::Gguf(model) => model.is_think_start_token(token),
Self::Qwen(model) => model.is_think_start_token(token),
}
}
fn is_think_end_token(self, token: i32) -> bool {
match self {
Self::Gguf(model) => model.is_think_end_token(token),
Self::Qwen(model) => model.is_think_end_token(token),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct ModelSummary {
pub(crate) model: ModelChoice,
@@ -320,18 +430,7 @@ impl Model {
&settings.artifacts,
)?;
if settings.model.is_qwen38() {
let context = u32::try_from(settings.context_tokens)
.map_err(|_| "Qwen context must be a positive whole number")?;
let loaded = qwen::load(&settings.artifacts.model, context, false)?;
return Err(format!(
"Qwen3.8 artifacts are valid, but its Rust Metal execution backend is not available until issue #95. Memory plan: {} resident core bytes, {} mapped PLE bytes, {} optional MTP bytes, {} KV/recurrent bytes, {} prefill transient bytes, {} admitted bytes.",
loaded.memory.resident_core,
loaded.memory.mapped_ple,
loaded.memory.optional_mtp,
loaded.memory.kv_and_recurrent,
loaded.memory.prefill_transient,
loaded.memory.admission,
));
return Err("Qwen must be opened through its dedicated safetensors loader".into());
}
let mut model = Self::open_main(&settings.artifacts.model, settings.model)?;
if settings.execution.warm_weights {
@@ -622,7 +721,7 @@ impl Generator {
pub(crate) fn open(settings: &EngineSettings, metrics: Arc<Metrics>) -> Result<Self, String> {
let simulated_memory =
SimulatedMemory::acquire(settings.diagnostics.simulated_used_memory_bytes)?;
let model = Model::open(settings)?;
let model = LoadedModel::open(settings)?;
let executor = metal::Executor::open_configured(
model,
settings.context_tokens.max(1) as u32,

View File

@@ -3,16 +3,18 @@ mod glm;
mod gpu;
mod hotlist;
mod profile;
mod qwen;
mod vision;
pub(super) use vision::VisionEmbedding;
use glm::GlmExecutor;
use gpu::*;
use profile::ExpertProfile;
use qwen::QwenExecutor;
use super::gguf::{BF16, F16, F32, Gguf, IQ2_XXS, MXFP4, Q2_K, Q4_K, Q8_0, Tensor as GgufTensor};
use super::validation::{DsparkConfig, SupportKind, dspark_config};
use super::{Model, ModelFamily, Rng, exact_delta_sample};
use super::{LoadedModel, Model, ModelFamily, ModelRef, Rng, exact_delta_sample};
use crate::model::ModelChoice;
use crate::settings::{
EngineSpeculativeSettings, EngineSsdSettings, EngineSteeringSettings, ReasoningMode,
@@ -44,7 +46,7 @@ fn environment_present(name: &CStr) -> bool {
!unsafe { getenv(name.as_ptr()) }.is_null()
}
const SOURCES: [(&str, &str); 22] = [
const SOURCES: [(&str, &str); 23] = [
("DS4_METAL_FLASH_ATTN_SOURCE", "flash_attn.metal"),
("DS4_METAL_DENSE_SOURCE", "dense.metal"),
("DS4_METAL_MOE_SOURCE", "moe.metal"),
@@ -67,6 +69,7 @@ const SOURCES: [(&str, &str); 22] = [
("DS4_METAL_GLM53_BF16_SOURCE", "glm53_bf16.metal"),
("DS4_METAL_GLM53_VISION_SOURCE", "glm53_vision.metal"),
("DS4_METAL_GLM53_KDA_SOURCE", "glm53_kda.metal"),
("DS4_METAL_QWEN38_SOURCE", "qwen38.metal"),
];
// The Metal boundary uses this only to decide whether diagnostic logs get ANSI
@@ -4360,15 +4363,17 @@ impl DeepSeekExecutor {
}
}
/// Model-family dispatch over the two Rust-owned Metal graphs.
/// Model-family dispatch over the Rust-owned Metal graphs.
pub(super) enum Executor {
DeepSeek(Box<DeepSeekExecutor>),
Glm(Box<GlmExecutor>),
Qwen(Box<QwenExecutor>),
}
pub(super) enum ResidentState {
DeepSeek(Box<DeepSeekResidentState>),
Glm(Box<glm::GlmResidentState>),
Qwen(Box<qwen::QwenResidentState>),
}
impl Executor {
@@ -4380,7 +4385,7 @@ impl Executor {
prefill_chunk: u32,
) -> Result<Self, String> {
Self::open_configured(
model,
LoadedModel::from(model),
context,
quality,
prefill_chunk,
@@ -4415,7 +4420,7 @@ impl Executor {
#[allow(clippy::too_many_arguments)]
pub(super) fn open_configured(
model: Model,
model: impl Into<LoadedModel>,
context: u32,
quality: bool,
prefill_chunk: u32,
@@ -4425,32 +4430,39 @@ impl Executor {
steering: EngineSteeringSettings,
expert_profile_path: Option<&str>,
) -> Result<Self, String> {
match model.shape.family {
ModelFamily::DeepSeek => DeepSeekExecutor::open_profile(
model,
context,
quality,
prefill_chunk,
power_percent,
speculative,
ssd,
steering,
expert_profile_path,
)
.map(Box::new)
.map(Self::DeepSeek),
ModelFamily::Glm => GlmExecutor::open_profile(
model,
context,
quality,
ssd,
speculative,
steering,
expert_profile_path,
)
.map(Box::new)
.map(Self::Glm),
ModelFamily::Qwen => unreachable!("Qwen uses its dedicated executor"),
match model.into() {
LoadedModel::Gguf(model) if model.shape.family == ModelFamily::DeepSeek => {
DeepSeekExecutor::open_profile(
*model,
context,
quality,
prefill_chunk,
power_percent,
speculative,
ssd,
steering,
expert_profile_path,
)
.map(Box::new)
.map(Self::DeepSeek)
}
LoadedModel::Gguf(model) if model.shape.family == ModelFamily::Glm => {
GlmExecutor::open_profile(
*model,
context,
quality,
ssd,
speculative,
steering,
expert_profile_path,
)
.map(Box::new)
.map(Self::Glm)
}
LoadedModel::Gguf(_) => unreachable!("Qwen never uses a GGUF model"),
LoadedModel::Qwen(model) => QwenExecutor::open(*model, context)
.map(Box::new)
.map(Self::Qwen),
}
}
@@ -4483,6 +4495,10 @@ impl Executor {
executor.eval(token)?;
Ok(vec![token])
}
Self::Qwen(executor) => {
executor.eval(token)?;
Ok(vec![token])
}
}
}
@@ -4490,6 +4506,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.eval(token),
Self::Glm(executor) => executor.eval(token),
Self::Qwen(executor) => executor.eval(token),
}
}
@@ -4499,7 +4516,7 @@ impl Executor {
) -> Result<Vec<vision::VisionEmbedding>, String> {
match self {
Self::Glm(executor) => executor.encode_visions(encoded),
Self::DeepSeek(_) => Err("vision input requires GLM 5.3 Flash".into()),
Self::DeepSeek(_) | Self::Qwen(_) => Err("vision input requires GLM 5.3 Flash".into()),
}
}
@@ -4510,7 +4527,8 @@ impl Executor {
match self {
Self::Glm(executor) => executor.set_vision_overlays(overlays),
Self::DeepSeek(_) if overlays.is_empty() => Ok(()),
Self::DeepSeek(_) => Err("vision input requires GLM 5.3 Flash".into()),
Self::Qwen(_) if overlays.is_empty() => Ok(()),
Self::DeepSeek(_) | Self::Qwen(_) => Err("vision input requires GLM 5.3 Flash".into()),
}
}
@@ -4529,6 +4547,11 @@ impl Executor {
let _ = reasoning;
executor.eval_speculative_greedy(token, max_tokens, cancelled)
}
Self::Qwen(executor) => {
let _ = (max_tokens, reasoning, cancelled);
executor.eval(token)?;
Ok(vec![token])
}
}
}
@@ -4540,6 +4563,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.prefill(tokens, progress),
Self::Glm(executor) => executor.prefill(tokens, progress),
Self::Qwen(executor) => executor.prefill(tokens, progress),
}
}
@@ -4547,6 +4571,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.logits(),
Self::Glm(executor) => executor.logits(),
Self::Qwen(executor) => executor.logits(),
}
}
@@ -4554,13 +4579,15 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.execution_stats(),
Self::Glm(executor) => executor.execution_stats(),
Self::Qwen(executor) => executor.execution_stats(),
}
}
pub(super) fn model(&self) -> &Model {
pub(super) fn model(&self) -> ModelRef<'_> {
match self {
Self::DeepSeek(executor) => executor.model(),
Self::Glm(executor) => executor.model(),
Self::DeepSeek(executor) => ModelRef::Gguf(executor.model()),
Self::Glm(executor) => ModelRef::Gguf(executor.model()),
Self::Qwen(executor) => ModelRef::Qwen(executor.model()),
}
}
@@ -4568,6 +4595,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.context(),
Self::Glm(executor) => executor.context(),
Self::Qwen(executor) => executor.context(),
}
}
@@ -4575,6 +4603,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.position(),
Self::Glm(executor) => executor.position(),
Self::Qwen(executor) => executor.position(),
}
}
@@ -4582,6 +4611,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.reset(),
Self::Glm(executor) => executor.reset(),
Self::Qwen(executor) => executor.reset(),
}
}
@@ -4596,6 +4626,9 @@ impl Executor {
Some(ResidentState::Glm(_)) => {
return Err("resident session belongs to a different model family".into());
}
Some(ResidentState::Qwen(_)) => {
return Err("resident session belongs to a different model family".into());
}
None => None,
};
executor.swap_resident_state(&mut inner)?;
@@ -4607,11 +4640,25 @@ impl Executor {
Some(ResidentState::DeepSeek(_)) => {
return Err("resident session belongs to a different model family".into());
}
Some(ResidentState::Qwen(_)) => {
return Err("resident session belongs to a different model family".into());
}
None => None,
};
executor.swap_resident_state(&mut inner)?;
*state = inner.map(|state| ResidentState::Glm(Box::new(state)));
}
Self::Qwen(executor) => {
let mut inner = match state.take() {
Some(ResidentState::Qwen(state)) => Some(*state),
Some(ResidentState::DeepSeek(_) | ResidentState::Glm(_)) => {
return Err("resident session belongs to a different model family".into());
}
None => None,
};
executor.swap_resident_state(&mut inner)?;
*state = inner.map(|state| ResidentState::Qwen(Box::new(state)));
}
}
Ok(())
}
@@ -4620,6 +4667,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.align_prompt(tokens),
Self::Glm(executor) => executor.align_prompt(tokens),
Self::Qwen(executor) => executor.align_prompt(tokens),
}
}
@@ -4627,6 +4675,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.tokens(),
Self::Glm(executor) => executor.tokens(),
Self::Qwen(executor) => executor.tokens(),
}
}
@@ -4634,6 +4683,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.checkpoint_tag(),
Self::Glm(executor) => executor.checkpoint_tag(),
Self::Qwen(executor) => executor.checkpoint_tag(),
}
}
@@ -4641,6 +4691,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.note_checkpoint_tag(tag),
Self::Glm(executor) => executor.note_checkpoint_tag(tag),
Self::Qwen(executor) => executor.note_checkpoint_tag(tag),
}
}
}

View File

@@ -320,6 +320,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.save_checkpoint(path, tag, progress),
Self::Glm(executor) => executor.save_checkpoint(path, tag, progress),
Self::Qwen(executor) => executor.save_checkpoint(path, tag, progress),
}
}
@@ -331,6 +332,7 @@ impl Executor {
match self {
Self::DeepSeek(executor) => executor.load_checkpoint(path, progress),
Self::Glm(executor) => executor.load_checkpoint(path, progress),
Self::Qwen(executor) => executor.load_checkpoint(path, progress),
}
}
}

View File

@@ -5,6 +5,22 @@ pub(super) struct GpuTensor {
_private: [u8; 0],
}
#[derive(Clone, Copy)]
#[repr(C)]
pub(super) struct QwenWeightView {
pub(super) map: *const c_void,
pub(super) size: u64,
pub(super) offset: u64,
pub(super) bytes: u64,
}
#[derive(Clone, Copy, Default)]
#[repr(C)]
pub(super) struct QwenKernelArgs {
pub(super) u: [u32; 16],
pub(super) f: [f32; 8],
}
#[derive(Clone, Copy, Default)]
#[repr(C)]
pub(super) struct Glm53VisionLayerWeights {
@@ -81,6 +97,18 @@ unsafe extern "C" {
map_size: u64,
max_tensor_bytes: u64,
) -> i32;
pub(super) fn ds4_gpu_qwen_dispatch(
kernel: *const c_char,
out: *mut GpuTensor,
a: *const GpuTensor,
b: *const GpuTensor,
c: *const GpuTensor,
weights: *const QwenWeightView,
weight_count: u32,
args: *const QwenKernelArgs,
grid_x: u32,
grid_y: u32,
) -> i32;
pub(super) fn ds4_gpu_set_transient_model_map_range(
model_map: *const c_void,
model_size: u64,
@@ -1804,7 +1832,7 @@ unsafe extern "C" {
}
pub(super) struct Context {
_model_file: File,
_model_file: Option<File>,
}
impl Context {
@@ -1908,9 +1936,29 @@ impl Context {
}
}
Ok(Self {
_model_file: model_file,
_model_file: Some(model_file),
})
}
pub(super) fn open_qwen(admission_bytes: u64) -> Result<Self, String> {
check(unsafe { ds4_gpu_init() }, "Metal initialization")?;
unsafe {
ds4_gpu_set_glm_model(false);
ds4_gpu_set_ssd_streaming(false);
ds4_gpu_set_decode_pipeline_fast_lookup(0);
}
let recommended = unsafe { ds4_gpu_recommended_working_set_size() };
if admission_bytes != 0 && recommended != 0 && admission_bytes > recommended {
unsafe { ds4_gpu_cleanup() };
return Err(format!(
"Qwen model load needs {:.1} GiB including context and scratch, but Metal recommends at most {:.1} GiB",
admission_bytes as f64 / 1_073_741_824.0,
recommended as f64 / 1_073_741_824.0,
));
}
unsafe { ds4_gpu_set_quality(true) };
Ok(Self { _model_file: None })
}
}
impl Drop for Context {

1862
src/engine/metal/qwen.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,13 @@
use super::tokenizer::Tokenizer;
use super::{ChatTurn, ModelSummary};
use crate::model::ModelChoice;
use crate::settings::ReasoningMode;
use memmap2::{Mmap, MmapOptions};
use serde::de::{MapAccess, Visitor};
use serde::{Deserialize, Deserializer};
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
use sha2::{Digest, Sha256};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::fmt;
use std::fs::{self, File};
use std::io::Read;
@@ -88,6 +93,158 @@ pub(super) struct ArtifactBindings {
pub(super) mtp: Vec<TensorBinding>,
}
pub(super) struct QwenMap {
path: PathBuf,
map: Mmap,
}
#[derive(Clone)]
pub(super) struct QwenTensor {
pub(super) map: usize,
pub(super) name: String,
pub(super) dtype: String,
pub(super) shape: Vec<u64>,
pub(super) quant_bits: Option<u32>,
pub(super) group_size: Option<u64>,
pub(super) range: std::ops::Range<u64>,
}
pub(super) struct QwenModel {
tokenizer: Tokenizer,
memory: MemoryPlan,
maps: Vec<QwenMap>,
tensors: HashMap<String, QwenTensor>,
identity: [u8; 32],
}
impl QwenModel {
pub(super) fn open(root: &Path, context: u32) -> Result<Self, String> {
let loaded = load(root, context, false)?;
let mut paths = loaded
.bindings
.core
.iter()
.map(|binding| binding.file.clone())
.collect::<Vec<_>>();
paths.sort();
paths.dedup();
let mut maps = Vec::with_capacity(paths.len());
let mut map_indices = HashMap::with_capacity(paths.len());
for path in paths {
let file = File::open(&path).map_err(|error| format!("{}: {error}", path.display()))?;
// SAFETY: verified managed artifacts remain read-only while the model owns each mapping.
let map = unsafe { MmapOptions::new().map(&file) }
.map_err(|error| format!("cannot map {}: {error}", path.display()))?;
map_indices.insert(path.clone(), maps.len());
maps.push(QwenMap { path, map });
}
let tensors = loaded
.bindings
.core
.into_iter()
.map(|binding| {
let tensor = QwenTensor {
map: map_indices[&binding.file],
name: binding.name.clone(),
dtype: binding.dtype,
shape: binding.shape,
quant_bits: binding.quant_bits,
group_size: binding.group_size,
range: binding.range,
};
(binding.name, tensor)
})
.collect::<HashMap<_, _>>();
let mut hash = Sha256::new();
hash.update(b"DS4Server Qwen3.8 checkpoint identity v1");
hash.update(MANIFEST);
let identity = hash.finalize().into();
Ok(Self {
tokenizer: loaded.tokenizer,
memory: loaded.memory,
maps,
tensors,
identity,
})
}
pub(super) fn tensor(&self, name: &str) -> Result<&QwenTensor, String> {
self.tensors
.get(name)
.ok_or_else(|| format!("Qwen core tensor is missing: {name}"))
}
pub(super) fn map(&self, index: usize) -> (&[u8], &Path) {
(&self.maps[index].map, &self.maps[index].path)
}
pub(super) fn checkpoint_identity(&self) -> [u8; 32] {
self.identity
}
pub(super) fn summary(&self) -> ModelSummary {
ModelSummary {
model: ModelChoice::Qwen38FlashNext,
mapped_bytes: self.maps.iter().map(|item| item.map.len() as u64).sum(),
tensor_count: self.tensors.len(),
vocabulary_size: self.tokenizer.vocab_size(),
support_loaded: false,
vision_loaded: false,
}
}
pub(super) fn render_conversation(
&self,
system: &str,
messages: &[ChatTurn],
reasoning: ReasoningMode,
) -> Vec<i32> {
self.tokenizer
.encode_conversation(system, messages, reasoning)
}
pub(super) fn render_history(
&self,
system: &str,
messages: &[ChatTurn],
reasoning: ReasoningMode,
) -> Vec<i32> {
self.tokenizer.encode_history(system, messages, reasoning)
}
pub(super) fn render_continuation(
&self,
prompt: &str,
reasoning: ReasoningMode,
skip_previous_eos: bool,
) -> Vec<i32> {
self.tokenizer
.encode_continuation(prompt, reasoning, skip_previous_eos)
}
pub(super) fn token_bytes(&self, token: i32) -> Option<Vec<u8>> {
self.tokenizer.token_bytes(token)
}
pub(super) fn is_stop_token_for_reasoning(&self, token: i32, reasoning: ReasoningMode) -> bool {
self.tokenizer.is_stop(token)
|| (reasoning == ReasoningMode::Direct
&& (self.tokenizer.is_think_start(token) || self.tokenizer.is_think_end(token)))
}
pub(super) fn is_think_start_token(&self, token: i32) -> bool {
self.tokenizer.is_think_start(token)
}
pub(super) fn is_think_end_token(&self, token: i32) -> bool {
self.tokenizer.is_think_end(token)
}
pub(super) fn memory(&self) -> &MemoryPlan {
&self.memory
}
}
pub(crate) fn validate_artifacts(root: &Path) -> Result<(), String> {
load(root, 262_144, true).map(|_| ())
}