Implement Qwen text core
This commit is contained in:
125
src/engine.rs
125
src/engine.rs
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
1862
src/engine/metal/qwen.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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(|_| ())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user