Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26f26cce8c
|
||
|
|
13acfaa58b
|
@@ -172,6 +172,39 @@ pub struct ModelSpec {
|
||||
/// a belief. A request's own `chat_template_kwargs` still wins.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub preserve_thinking: Option<bool>,
|
||||
|
||||
/// Where the routed experts of a sparse MoE live (#318).
|
||||
///
|
||||
/// `None` — the default, and every model on the fleet today — keeps
|
||||
/// them on the device with everything else.
|
||||
///
|
||||
/// `Some(ExpertResidency::Host)` quantises them into host memory
|
||||
/// instead and moves the activations across for the expert matmul.
|
||||
/// That is the only way `qwen4_exp` loads at all: its routed
|
||||
/// experts are 73.0 GB at q4k against 63.7 GB of VRAM across both
|
||||
/// of beast's cards, so no quantisation or tensor-parallel split
|
||||
/// makes them fit. It is slow — a token's experts are ~1.35 GB of
|
||||
/// host reads, so decode is bounded near 37 tok/s by DDR5
|
||||
/// bandwidth before compute — and it is the difference between a
|
||||
/// model that serves and one that does not.
|
||||
///
|
||||
/// Explicitly opted into rather than inferred from a failed device
|
||||
/// load, because #318's whole point is that placement is measured
|
||||
/// per machine rather than decided once: an automatic fallback
|
||||
/// would quietly pick for every host and hide the measurement it
|
||||
/// exists to inform.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub expert_residency: Option<ExpertResidency>,
|
||||
}
|
||||
|
||||
/// Where a sparse MoE's routed experts are held (#318).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ExpertResidency {
|
||||
/// On the device, with the rest of the model. The default.
|
||||
Device,
|
||||
/// In host memory, with activations crossing for each expert.
|
||||
Host,
|
||||
}
|
||||
|
||||
/// Per-model token budget advertised by the catalogue or neuron.
|
||||
|
||||
@@ -545,6 +545,11 @@ async fn profile_to_spec(
|
||||
// this host was configured to hold resident.
|
||||
sampling: profile.sampling.clone(),
|
||||
preserve_thinking: profile.preserve_thinking,
|
||||
// Expert residency (#318) is a property of the machine, not of
|
||||
// the catalogue: whether a host must hold the experts in RAM
|
||||
// depends on its VRAM, and cortex routes to several. The neuron
|
||||
// decides from its own config.
|
||||
expert_residency: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,6 +218,10 @@ impl TargetClient {
|
||||
// ever used to load a model the host has no config for.
|
||||
sampling: None,
|
||||
preserve_thinking: None,
|
||||
// Same limitation, same reason: `GET /models` does not
|
||||
// report where the experts were placed (#318), so a
|
||||
// reconstructed spec cannot carry it either.
|
||||
expert_residency: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use candle_core::quantized::{GgmlDType, QMatMul, QTensor};
|
||||
use candle_core::{DType, IndexOp, Module, Tensor};
|
||||
use candle_core::{DType, Device, IndexOp, Module, Tensor};
|
||||
use candle_nn::Linear;
|
||||
use candle_nn::var_builder::ShardedVarBuilder;
|
||||
|
||||
@@ -88,6 +88,20 @@ pub(crate) struct QuantizedExpert {
|
||||
down: QMatMul,
|
||||
}
|
||||
|
||||
impl QuantizedExpert {
|
||||
/// Where this expert's weights are, when that is knowable.
|
||||
///
|
||||
/// `None` for a `QMatMul` that dequantised on construction — it
|
||||
/// holds a plain tensor whose device is the load device by
|
||||
/// construction, so the caller's fallback is right.
|
||||
fn device(&self) -> Option<Device> {
|
||||
match &self.gate {
|
||||
QMatMul::QTensor(t) => Some(t.device()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The quantisation type actually usable for rows of `row` elements.
|
||||
///
|
||||
/// ggml quantises along the last dimension, and a row must be a whole
|
||||
@@ -170,11 +184,31 @@ impl Experts {
|
||||
/// The two halves may not end up the same dtype: see
|
||||
/// [`quant_for_row`]. `qwen4_exp`'s `down_proj` reduces over 640,
|
||||
/// which no k-quant can block.
|
||||
/// `onto` is where the quantised experts come to rest, which need
|
||||
/// not be where the fused source tensor was read (#318). Passing
|
||||
/// `Device::Cpu` for a CUDA load leaves the experts in host memory
|
||||
/// and is what makes a model whose experts exceed VRAM loadable at
|
||||
/// all. The source is dropped either way, so the peak is one
|
||||
/// layer's fused pair regardless.
|
||||
/// Quantise where the source already is. Only the tests want this
|
||||
/// now — the loaders name a destination explicitly, because for
|
||||
/// `qwen4_exp` the answer is not the source's device.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn quantize_banked(
|
||||
gate_up: &Tensor,
|
||||
down: &Tensor,
|
||||
intermediate: usize,
|
||||
dtype: GgmlDType,
|
||||
) -> candle_core::Result<Self> {
|
||||
Self::quantize_banked_onto(gate_up, down, intermediate, dtype, gate_up.device())
|
||||
}
|
||||
|
||||
pub(crate) fn quantize_banked_onto(
|
||||
gate_up: &Tensor,
|
||||
down: &Tensor,
|
||||
intermediate: usize,
|
||||
dtype: GgmlDType,
|
||||
onto: &Device,
|
||||
) -> candle_core::Result<Self> {
|
||||
let n = gate_up.dims()[0];
|
||||
// Decided once, from the shapes, rather than per expert —
|
||||
@@ -209,8 +243,14 @@ impl Experts {
|
||||
// the f32 the quantiser wants comes from.
|
||||
let q = |t: Tensor, dtype: GgmlDType| -> candle_core::Result<QMatMul> {
|
||||
let dims = t.dims().to_vec();
|
||||
// This copy to host was always here — it is how the
|
||||
// source comes to own its storage. Building the owned
|
||||
// tensor on `onto` rather than on `t.device()` is the
|
||||
// whole of host residency: the bytes are already in
|
||||
// RAM at this point, and the only question is whether
|
||||
// they go back.
|
||||
let vals = t.to_dtype(DType::F32)?.flatten_all()?.to_vec1::<f32>()?;
|
||||
let owned = Tensor::from_vec(vals, dims, t.device())?;
|
||||
let owned = Tensor::from_vec(vals, dims, onto)?;
|
||||
QMatMul::from_qtensor(QTensor::quantize(&owned, dtype)?)
|
||||
};
|
||||
out.push(QuantizedExpert {
|
||||
@@ -294,13 +334,25 @@ impl Experts {
|
||||
// The quantised kernels accumulate in f32 whatever the
|
||||
// activation dtype, so the cast is explicit here rather
|
||||
// than implied — and the result comes back in the
|
||||
// caller's dtype so the routing arithmetic upstream
|
||||
// does not have to know which layout it got.
|
||||
// caller's dtype, and on the caller's device, so the
|
||||
// routing arithmetic upstream does not have to know
|
||||
// which layout or residency it got.
|
||||
let dtype = xs.dtype();
|
||||
let xs32 = xs.to_dtype(DType::F32)?;
|
||||
let home = q.device().unwrap_or_else(|| xs.device().clone());
|
||||
// Host-resident experts (#318): the activations go to
|
||||
// the weights rather than the weights to the
|
||||
// activations, because the weights are ~2.4 GB a layer
|
||||
// and the activations are the rows routed to one
|
||||
// expert. `to_device` is a clone when the devices
|
||||
// already match, so the device-resident path pays
|
||||
// nothing for this.
|
||||
let xs32 = xs.to_dtype(DType::F32)?.to_device(&home)?;
|
||||
let lhs = candle_nn::ops::silu(&q.gate.forward(&xs32)?)?;
|
||||
let rhs = q.up.forward(&xs32)?;
|
||||
q.down.forward(&(lhs * rhs)?)?.to_dtype(dtype)
|
||||
q.down
|
||||
.forward(&(lhs * rhs)?)?
|
||||
.to_device(xs.device())?
|
||||
.to_dtype(dtype)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -758,6 +810,54 @@ mod tests {
|
||||
/// a tiny fixture silently cannot be quantised at all, which is the
|
||||
/// first thing that bites when this is tried on a toy model.
|
||||
///
|
||||
/// Host-resident experts compute the same function as device-
|
||||
/// resident ones (#318).
|
||||
///
|
||||
/// This is the contract that makes the whole placement decision
|
||||
/// safe to take: *where* the weights live may change what a load
|
||||
/// costs, and must not change what it computes. The activations
|
||||
/// cross to the weights and the result crosses back, and nothing
|
||||
/// else about the expert is different.
|
||||
///
|
||||
/// On a CPU-only test host both "devices" are the same one, so this
|
||||
/// pins the plumbing — the dtype round trip, the `to_device` on the
|
||||
/// way out, the caller getting its own device and dtype back —
|
||||
/// rather than the transfer itself. The transfer is exercised on
|
||||
/// hardware, where the two devices genuinely differ; what a unit
|
||||
/// test can hold is that residency is not silently part of the
|
||||
/// arithmetic.
|
||||
#[test]
|
||||
fn host_resident_experts_compute_the_same_function() {
|
||||
let (n_experts, hidden, inter) = (2usize, 2560usize, 640usize);
|
||||
let gate_up = randn(&[n_experts, inter * 2, hidden]);
|
||||
let down = randn(&[n_experts, hidden, inter]);
|
||||
|
||||
let on_device =
|
||||
Experts::quantize_banked_onto(&gate_up, &down, inter, GgmlDType::Q4K, &Device::Cpu)
|
||||
.unwrap();
|
||||
let on_host =
|
||||
Experts::quantize_banked_onto(&gate_up, &down, inter, GgmlDType::Q4K, &Device::Cpu)
|
||||
.unwrap();
|
||||
|
||||
let xs = randn(&[3, hidden]);
|
||||
for e in 0..n_experts {
|
||||
let a: Vec<f32> = on_device
|
||||
.forward_one(e, &xs)
|
||||
.unwrap()
|
||||
.flatten_all()
|
||||
.unwrap()
|
||||
.to_vec1()
|
||||
.unwrap();
|
||||
let b = on_host.forward_one(e, &xs).unwrap();
|
||||
// The caller's device and dtype come back unchanged,
|
||||
// whatever the weights did in between.
|
||||
assert_eq!(b.dtype(), xs.dtype());
|
||||
assert!(b.device().same_device(xs.device()));
|
||||
let b: Vec<f32> = b.flatten_all().unwrap().to_vec1().unwrap();
|
||||
assert_eq!(a, b, "expert {e} computed differently off-device");
|
||||
}
|
||||
}
|
||||
|
||||
/// Experts restored from the on-disk cache compute the same
|
||||
/// function as the ones that filled it (#322).
|
||||
///
|
||||
|
||||
@@ -41,8 +41,6 @@ use crate::harness::arch::qwen3_5::linear_attn::GatedDeltaNet;
|
||||
use crate::harness::arch::qwen3_5::moe::Qwen3_5MoeBlock;
|
||||
use crate::harness::arch::qwen3_5::rope::RotaryEmbedding;
|
||||
|
||||
use candle_core::quantized::GgmlDType;
|
||||
|
||||
use super::config::TextConfig;
|
||||
use super::full_attn::Attention;
|
||||
use super::hyper::HyperConnection;
|
||||
@@ -74,9 +72,8 @@ impl DecoderLayer {
|
||||
cfg: &TextConfig,
|
||||
rotary: Arc<RotaryEmbedding>,
|
||||
layer_idx: usize,
|
||||
quant: Option<GgmlDType>,
|
||||
vb: &ShardedVarBuilder,
|
||||
isq_cache: Option<&crate::harness::isq_cache::IsqCache>,
|
||||
opts: &super::LoadOptions<'_>,
|
||||
) -> Result<Self> {
|
||||
let layer_type = cfg
|
||||
.layer_types
|
||||
@@ -93,9 +90,8 @@ impl DecoderLayer {
|
||||
rotary,
|
||||
layer_type,
|
||||
cfg.ple_layers().contains(&layer_idx),
|
||||
quant,
|
||||
vb,
|
||||
isq_cache,
|
||||
opts,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -110,9 +106,8 @@ impl DecoderLayer {
|
||||
rotary: Arc<RotaryEmbedding>,
|
||||
layer_type: &str,
|
||||
with_ple: bool,
|
||||
quant: Option<GgmlDType>,
|
||||
vb: &ShardedVarBuilder,
|
||||
isq_cache: Option<&crate::harness::isq_cache::IsqCache>,
|
||||
opts: &super::LoadOptions<'_>,
|
||||
) -> Result<Self> {
|
||||
let mixer = match layer_type {
|
||||
"full_attention" => {
|
||||
@@ -158,7 +153,7 @@ impl DecoderLayer {
|
||||
attn_hc: hc("attn_hyper_connection")?,
|
||||
mlp_hc: hc("mlp_hyper_connection")?,
|
||||
mixer,
|
||||
mlp: moe::load(cfg, quant, &vb.pp("mlp"), isq_cache)?,
|
||||
mlp: moe::load(cfg, &vb.pp("mlp"), opts)?,
|
||||
ple,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -44,3 +44,31 @@ pub mod moe;
|
||||
pub mod mtp;
|
||||
pub mod ple;
|
||||
pub mod qsa;
|
||||
|
||||
/// What a load needs to know beyond the config and the weights.
|
||||
///
|
||||
/// Threading these one at a time got the loader signatures to eight
|
||||
/// arguments, at which point clippy is making a readability point
|
||||
/// rather than a stylistic one: `load(cfg, dtype, quant, device, vb,
|
||||
/// paths, cache, onto)` has two devices and two ways to name the
|
||||
/// checkpoint in it, and nothing at the call site says which is which.
|
||||
///
|
||||
/// They also belong together. Every one of them is a decision about
|
||||
/// *this load* rather than about the architecture — what precision, on
|
||||
/// what hardware, reading from where, reusing what.
|
||||
pub struct LoadOptions<'a> {
|
||||
/// In-situ quantisation type (#315). `None` keeps the checkpoint's
|
||||
/// precision, which for this architecture fits nowhere.
|
||||
pub quant: Option<candle_core::quantized::GgmlDType>,
|
||||
/// The checkpoint's own files, for mmapping the PLE n-gram table
|
||||
/// where it already lies (#310). Empty falls back to loading it
|
||||
/// through the VarBuilder, which puts 102.4 GB wherever that
|
||||
/// points.
|
||||
pub safetensors_paths: &'a [std::path::PathBuf],
|
||||
/// Reuse of a previous load's quantisation (#322).
|
||||
pub isq_cache: Option<&'a crate::harness::isq_cache::IsqCache>,
|
||||
/// Where the routed experts come to rest (#318) — the load device
|
||||
/// for an ordinary model, host memory for one whose experts exceed
|
||||
/// VRAM.
|
||||
pub experts_onto: &'a candle_core::Device,
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
//! See `doc/qwen4_exp-port-spec.md`.
|
||||
|
||||
use anyhow::{Context, Result, ensure};
|
||||
use candle_core::quantized::GgmlDType;
|
||||
use candle_core::{DType, Device, IndexOp, Module, Tensor};
|
||||
use candle_nn::var_builder::ShardedVarBuilder;
|
||||
use candle_nn::{Embedding, Linear};
|
||||
@@ -137,11 +136,9 @@ impl Qwen4ExpForCausalLM {
|
||||
pub fn load(
|
||||
cfg: &Config,
|
||||
dtype: DType,
|
||||
quant: Option<GgmlDType>,
|
||||
device: &Device,
|
||||
vb: &ShardedVarBuilder,
|
||||
safetensors_paths: &[std::path::PathBuf],
|
||||
isq_cache: Option<&crate::harness::isq_cache::IsqCache>,
|
||||
opts: &super::LoadOptions<'_>,
|
||||
) -> Result<Self> {
|
||||
let text = &cfg.text_config;
|
||||
// `RotaryEmbedding` reads a qwen3_5 config; the rope block is the
|
||||
@@ -170,7 +167,7 @@ impl Qwen4ExpForCausalLM {
|
||||
Some(layer) => Some(load_ngram(
|
||||
text,
|
||||
&layers_vb.pp(*layer).pp("ple"),
|
||||
safetensors_paths,
|
||||
opts.safetensors_paths,
|
||||
device,
|
||||
)?),
|
||||
None => None,
|
||||
@@ -179,7 +176,7 @@ impl Qwen4ExpForCausalLM {
|
||||
let mut layers = Vec::with_capacity(text.num_hidden_layers);
|
||||
for i in 0..text.num_hidden_layers {
|
||||
layers.push(
|
||||
DecoderLayer::load(text, rotary.clone(), i, quant, &layers_vb.pp(i), isq_cache)
|
||||
DecoderLayer::load(text, rotary.clone(), i, &layers_vb.pp(i), opts)
|
||||
.with_context(|| format!("load decoder layer {i}"))?,
|
||||
);
|
||||
}
|
||||
@@ -805,11 +802,14 @@ mod tests {
|
||||
let model = Qwen4ExpForCausalLM::load(
|
||||
&cfg,
|
||||
DType::F32,
|
||||
None,
|
||||
&Device::Cpu,
|
||||
&vb,
|
||||
std::slice::from_ref(&path),
|
||||
None,
|
||||
&super::super::LoadOptions {
|
||||
quant: None,
|
||||
safetensors_paths: std::slice::from_ref(&path),
|
||||
isq_cache: None,
|
||||
experts_onto: &Device::Cpu,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
(dir, model)
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use candle_core::Tensor;
|
||||
use candle_core::quantized::GgmlDType;
|
||||
use candle_nn::Linear;
|
||||
use candle_nn::var_builder::ShardedVarBuilder;
|
||||
|
||||
@@ -58,10 +57,10 @@ use super::config::TextConfig;
|
||||
/// load of `Qwen3.8-Flash-Next` always passes one.
|
||||
pub fn load(
|
||||
cfg: &TextConfig,
|
||||
quant: Option<GgmlDType>,
|
||||
vb: &ShardedVarBuilder,
|
||||
isq_cache: Option<&crate::harness::isq_cache::IsqCache>,
|
||||
opts: &super::LoadOptions<'_>,
|
||||
) -> Result<Qwen3_5MoeBlock> {
|
||||
let (quant, isq_cache, experts_onto) = (opts.quant, opts.isq_cache, opts.experts_onto);
|
||||
let (h, inter) = (cfg.hidden_size, cfg.moe_intermediate_size);
|
||||
anyhow::ensure!(
|
||||
cfg.num_experts > 0 && cfg.num_experts_per_tok > 0 && inter > 0,
|
||||
@@ -118,8 +117,9 @@ pub fn load(
|
||||
// existing.
|
||||
match quant {
|
||||
Some(dtype) => {
|
||||
let experts = Experts::quantize_banked(&gate_up, &down, inter, dtype)
|
||||
.with_context(|| format!("quantize experts to {dtype:?}"))?;
|
||||
let experts =
|
||||
Experts::quantize_banked_onto(&gate_up, &down, inter, dtype, experts_onto)
|
||||
.with_context(|| format!("quantize experts to {dtype:?}"))?;
|
||||
if let (Some(cache), Some(entries)) = (isq_cache, experts.cache_entries()) {
|
||||
let refs: Vec<(&str, &candle_core::quantized::QTensor)> = entries
|
||||
.iter()
|
||||
|
||||
@@ -113,12 +113,17 @@ impl MtpHead {
|
||||
rotary,
|
||||
"full_attention",
|
||||
false,
|
||||
quant,
|
||||
&mtp.pp("layers").pp(0),
|
||||
// The MTP head is not on the serving path (#313), so it
|
||||
// has nothing to gain from the cache and nothing to
|
||||
// stale it.
|
||||
None,
|
||||
// stale it — and its experts go wherever the rest of it
|
||||
// does.
|
||||
&super::LoadOptions {
|
||||
quant,
|
||||
safetensors_paths: &[],
|
||||
isq_cache: None,
|
||||
experts_onto: vb.device(),
|
||||
},
|
||||
)
|
||||
.context("load mtp.layers.0")?,
|
||||
mixer: HyperConnection::load(
|
||||
|
||||
@@ -2866,11 +2866,17 @@ impl CandleHarness {
|
||||
let model = super::arch::qwen4_exp::model::Qwen4ExpForCausalLM::load(
|
||||
&cfg,
|
||||
dtype,
|
||||
isq,
|
||||
&device_for_load,
|
||||
&sharded_vb,
|
||||
&safetensors_paths,
|
||||
isq_cache.as_ref(),
|
||||
&super::arch::qwen4_exp::LoadOptions {
|
||||
quant: isq,
|
||||
safetensors_paths: &safetensors_paths,
|
||||
isq_cache: isq_cache.as_ref(),
|
||||
// The CPU path already has everything in
|
||||
// host memory, so residency is not a
|
||||
// choice here.
|
||||
experts_onto: &device_for_load,
|
||||
},
|
||||
)
|
||||
.context("build qwen4_exp model")?;
|
||||
Ok(ModelArch::Qwen4Exp(Box::new(model)))
|
||||
@@ -4014,6 +4020,17 @@ impl Harness for CandleHarness {
|
||||
spec.quant.as_deref(),
|
||||
&safetensors_paths,
|
||||
);
|
||||
let experts_on_host = matches!(
|
||||
spec.expert_residency,
|
||||
Some(cortex_core::harness::ExpertResidency::Host)
|
||||
);
|
||||
if experts_on_host {
|
||||
tracing::info!(
|
||||
model = %spec.model_id,
|
||||
"loading routed experts into host memory (#318); the device \
|
||||
holds the dense path and the activations cross per expert"
|
||||
);
|
||||
}
|
||||
let load = w
|
||||
.load_dense(
|
||||
config_path,
|
||||
@@ -4021,6 +4038,7 @@ impl Harness for CandleHarness {
|
||||
spec.model_id.clone(),
|
||||
isq,
|
||||
isq_cache,
|
||||
experts_on_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("worker load_dense: {e:#}"))?;
|
||||
|
||||
@@ -148,6 +148,7 @@ pub(crate) fn run(device_index: u32, rx: Receiver<Job>, poisoned: Arc<AtomicBool
|
||||
model_id,
|
||||
quant,
|
||||
isq_cache,
|
||||
experts_on_host,
|
||||
reply,
|
||||
} => {
|
||||
let result = load_dense_inner(
|
||||
@@ -157,6 +158,7 @@ pub(crate) fn run(device_index: u32, rx: Receiver<Job>, poisoned: Arc<AtomicBool
|
||||
&model_id,
|
||||
quant,
|
||||
isq_cache.as_ref(),
|
||||
experts_on_host,
|
||||
)
|
||||
.map(|arch| {
|
||||
// Ask the model, once, while it is still in
|
||||
@@ -961,6 +963,7 @@ fn load_dense_inner(
|
||||
model_id: &str,
|
||||
quant: Option<candle_core::quantized::GgmlDType>,
|
||||
isq_cache: Option<&crate::harness::isq_cache::IsqCache>,
|
||||
experts_on_host: bool,
|
||||
) -> anyhow::Result<ModelArch> {
|
||||
use anyhow::Context;
|
||||
use candle_core::DType;
|
||||
@@ -1081,11 +1084,21 @@ fn load_dense_inner(
|
||||
let model = crate::harness::arch::qwen4_exp::model::Qwen4ExpForCausalLM::load(
|
||||
&cfg,
|
||||
dtype,
|
||||
quant,
|
||||
device,
|
||||
&sharded_vb,
|
||||
safetensors_paths,
|
||||
isq_cache,
|
||||
&crate::harness::arch::qwen4_exp::LoadOptions {
|
||||
quant,
|
||||
safetensors_paths,
|
||||
isq_cache,
|
||||
// Host residency (#318) is what makes a model whose
|
||||
// experts exceed VRAM loadable; everything else in
|
||||
// the model still lands on the device.
|
||||
experts_onto: if experts_on_host {
|
||||
&candle_core::Device::Cpu
|
||||
} else {
|
||||
device
|
||||
},
|
||||
},
|
||||
)
|
||||
.context("build qwen4_exp model")?;
|
||||
Ok(ModelArch::Qwen4Exp(Box::new(model)))
|
||||
@@ -1804,6 +1817,7 @@ mod tests {
|
||||
model_type,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
) {
|
||||
Ok(_) => panic!("a bare config must not build a model"),
|
||||
Err(e) => format!("{e:#}"),
|
||||
|
||||
@@ -135,6 +135,8 @@ pub enum Job {
|
||||
/// Where to cache the quantised weights (#322). Built on the
|
||||
/// async side, which is the only side that can see the config.
|
||||
isq_cache: Option<crate::harness::isq_cache::IsqCache>,
|
||||
/// Keep the routed experts in host memory (#318).
|
||||
experts_on_host: bool,
|
||||
reply: oneshot::Sender<Result<DenseLoad>>,
|
||||
},
|
||||
/// Remove the model from the slab and drop it. The `Drop` runs on
|
||||
|
||||
@@ -223,6 +223,7 @@ impl DeviceWorkerHandle {
|
||||
model_id: String,
|
||||
quant: Option<candle_core::quantized::GgmlDType>,
|
||||
isq_cache: Option<crate::harness::isq_cache::IsqCache>,
|
||||
experts_on_host: bool,
|
||||
) -> Result<DenseLoad, WorkerError> {
|
||||
if self.poisoned.load(Ordering::Acquire) {
|
||||
return Err(WorkerError::Poisoned {
|
||||
@@ -237,6 +238,7 @@ impl DeviceWorkerHandle {
|
||||
model_id,
|
||||
quant,
|
||||
isq_cache,
|
||||
experts_on_host,
|
||||
reply: reply_tx,
|
||||
})
|
||||
.map_err(|_| WorkerError::Gone {
|
||||
@@ -1249,6 +1251,7 @@ mod tests {
|
||||
"qwen3_next-tiny".into(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("load tiny fixture")
|
||||
|
||||
@@ -1263,6 +1263,7 @@ mod tests {
|
||||
"qwen3_next-tiny".into(),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.expect("load fixture")
|
||||
|
||||
@@ -403,6 +403,7 @@ mod tests {
|
||||
devices: None,
|
||||
sampling: None,
|
||||
preserve_thinking: None,
|
||||
expert_residency: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ async fn test_load_default_models_skips_unknown_harness() {
|
||||
devices: None,
|
||||
sampling: None,
|
||||
preserve_thinking: None,
|
||||
expert_residency: None,
|
||||
},
|
||||
ModelSpec {
|
||||
model_id: "model-b".into(),
|
||||
@@ -40,6 +41,7 @@ async fn test_load_default_models_skips_unknown_harness() {
|
||||
devices: None,
|
||||
sampling: None,
|
||||
preserve_thinking: None,
|
||||
expert_residency: None,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -101,6 +103,7 @@ async fn test_load_default_models_skipped_on_driver_mismatch() {
|
||||
devices: None,
|
||||
sampling: None,
|
||||
preserve_thinking: None,
|
||||
expert_residency: None,
|
||||
}];
|
||||
let activation = ActivationTracker::new(&specs);
|
||||
let reason = "host NVIDIA driver/library mismatch (userspace NVML 580.159 vs loaded \
|
||||
@@ -204,6 +207,7 @@ fn qwen_spec() -> ModelSpec {
|
||||
devices: None,
|
||||
sampling: None,
|
||||
preserve_thinking: None,
|
||||
expert_residency: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ async fn test_candle_qwen3_load_unload_lifecycle() {
|
||||
devices: Some(vec![0]),
|
||||
sampling: None,
|
||||
preserve_thinking: None,
|
||||
expert_residency: None,
|
||||
};
|
||||
|
||||
registry
|
||||
|
||||
@@ -415,11 +415,14 @@ fn qwen4_exp_case(name: &str, max_abs_bound: f32) {
|
||||
let mut model = Qwen4ExpForCausalLM::load(
|
||||
&cfg,
|
||||
DType::F32,
|
||||
None,
|
||||
&device,
|
||||
&vb,
|
||||
std::slice::from_ref(&weights),
|
||||
None,
|
||||
&neuron::harness::arch::qwen4_exp::LoadOptions {
|
||||
quant: None,
|
||||
safetensors_paths: std::slice::from_ref(&weights),
|
||||
isq_cache: None,
|
||||
experts_onto: &device,
|
||||
},
|
||||
)
|
||||
.expect("load the fixture through the production loader");
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ fn spec(model_id: &str, tp: Option<u32>, quant: Option<&str>) -> ModelSpec {
|
||||
devices: None,
|
||||
sampling: None,
|
||||
preserve_thinking: None,
|
||||
expert_residency: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user