spark_runtime/weights/
k3.rs1use super::{WeightLoader, WeightStore, WeightTensor, evict_page_cache};
5use crate::gpu::GpuBackend;
6use anyhow::{Context, Result, ensure};
7use avarok_core::config::ModelConfig;
8use avarok_core::kimi_k3::tp::TpAxis;
9use std::path::Path;
10
11mod index;
12mod inventory;
13#[cfg(test)]
14mod tests;
15
16pub struct K3SafetensorsLoader {
17 config: ModelConfig,
18}
19impl K3SafetensorsLoader {
20 pub fn new(config: ModelConfig) -> Result<Self> {
21 ensure!(
22 matches!(
23 config.model_type.as_str(),
24 "kimi_k3" | "kimi_linear" | "Kimi-K3"
25 ),
26 "K3 rank loader requires a K3 configuration"
27 );
28 ensure!(
29 config.tp_world_size > 0 && config.tp_rank < config.tp_world_size,
30 "invalid K3 TP topology"
31 );
32 Ok(Self { config })
33 }
34}
35impl WeightStore {
36 pub fn prepartitioned_tp(&self) -> Option<(usize, usize)> {
38 self.prepartitioned_tp
39 }
40}
41
42impl WeightLoader for K3SafetensorsLoader {
43 fn load(&self, model_dir: &Path, gpu: &dyn GpuBackend, reserve: usize) -> Result<WeightStore> {
44 let inventory = inventory::scan(model_dir, &self.config)?;
45 let mut local_bytes = 0usize;
46 let mut source_bytes = 0usize;
47 let mut replicated_bytes = 0usize;
48 let mut staging_peak = 0usize;
49 let mut binding_extra_bytes = 0usize;
50 for file in &inventory {
51 for tensor in &file.tensors {
52 local_bytes = local_bytes
53 .checked_add(tensor.plan.local_bytes())
54 .context("K3 local byte overflow")?;
55 source_bytes = source_bytes
56 .checked_add(tensor.plan.source_bytes)
57 .context("K3 source byte overflow")?;
58 if tensor.plan.axis == TpAxis::Replicated {
59 replicated_bytes = replicated_bytes
60 .checked_add(tensor.plan.local_bytes())
61 .context("K3 replicated byte overflow")?;
62 }
63 staging_peak = staging_peak.max(tensor.plan.local_bytes());
64 binding_extra_bytes = binding_extra_bytes
65 .checked_add(avarok_core::kimi_k3::binding_memory::extra_gpu_bytes(
66 &tensor.name,
67 tensor.dtype == super::WeightDtype::FP32,
68 tensor.plan.local_bytes() / tensor.dtype.byte_size(),
69 )?)
70 .context("K3 binding byte overflow")?;
71 }
72 }
73 let admitted = local_bytes
77 .checked_add(binding_extra_bytes)
78 .and_then(|n| n.checked_add(staging_peak))
79 .and_then(|n| n.checked_add(reserve))
80 .context("K3 admission byte overflow")?;
81 let initial_free = gpu.free_memory()?;
82 tracing::info!(
83 event = "k3_weight_plan",
84 rank = self.config.tp_rank,
85 world = self.config.tp_world_size,
86 source_bytes,
87 local_bytes,
88 replicated_bytes,
89 staging_peak,
90 binding_extra_bytes,
91 reserve,
92 admitted,
93 free_bytes = initial_free,
94 "K3 pre-upload memory plan; engine workspace/KV/runtime require explicit reserve"
95 );
96 ensure!(
97 admitted <= initial_free,
98 "K3 OOM preflight: rank {} needs {} bytes (local {}, replicated {}, staging {}, binding copies {}, reserve {}), free {}",
99 self.config.tp_rank,
100 admitted,
101 local_bytes,
102 replicated_bytes,
103 staging_peak,
104 binding_extra_bytes,
105 reserve,
106 initial_free
107 );
108 let mut store = WeightStore::empty();
109 let mut uploaded = 0usize;
110 let result: Result<()> = (|| {
111 for item in inventory {
112 let file = std::fs::File::open(&item.path)?;
113 let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };
114 let tensors = safetensors::SafeTensors::deserialize(&mmap)?;
115 for spec in item.tensors {
116 let tensor = tensors.tensor(&spec.name)?;
117 ensure!(
118 tensor.shape() == spec.source_shape && tensor.dtype() == spec.source_dtype,
119 "{}: tensor metadata changed after preflight",
120 spec.name
121 );
122 let local = spec.plan.copy_shard(tensor.data())?;
123 let ptr = gpu.alloc(local.len()).with_context(|| {
125 format!(
126 "K3 rank {} allocating {} local bytes for {}",
127 self.config.tp_rank,
128 local.len(),
129 spec.name
130 )
131 })?;
132 if let Err(e) = gpu.copy_h2d(&local, ptr) {
133 let _ = gpu.free(ptr);
134 return Err(e);
135 }
136 uploaded += local.len();
137 store.weights.insert(
138 spec.name,
139 WeightTensor {
140 ptr,
141 shape: spec.plan.local_shape,
142 dtype: spec.dtype,
143 },
144 );
145 }
146 drop(tensors);
147 drop(mmap);
148 evict_page_cache(&file);
149 let free = gpu.free_memory()?;
150 tracing::info!(
151 event = "k3_weight_shard_loaded",
152 rank = self.config.tp_rank,
153 uploaded_bytes = uploaded,
154 free_bytes = free,
155 sampled_device_delta_bytes = initial_free.saturating_sub(free),
156 "K3 rank-local shard uploaded"
157 );
158 ensure!(
159 free >= reserve,
160 "K3 reserve exhausted during rank-local upload"
161 );
162 }
163 Ok(())
164 })();
165 if let Err(err) = result {
166 for (name, tensor) in store.weights.drain() {
167 if let Err(cleanup) = gpu.free(tensor.ptr) {
168 tracing::error!(%cleanup,%name,"K3 allocation cleanup failed");
169 }
170 }
171 return Err(err);
172 }
173 ensure!(uploaded == local_bytes, "K3 upload accounting mismatch");
174 store.prepartitioned_tp = Some((self.config.tp_rank, self.config.tp_world_size));
175 Ok(store)
176 }
177}