1use crate::pool::Pool;
25use crate::qtensor::{QTensor, sgemm_public};
26use cortiq_core::quant::f16_to_f32;
27use cortiq_core::format::SelectionDescriptor;
28use cortiq_core::{CmfModel, TensorDtype};
29use serde::{Deserialize, Serialize};
30use std::path::{Path, PathBuf};
31use std::sync::Arc;
32
33pub const SIDECAR_ARCH: &str = "dflash2_sidecar";
34pub const ROUTE_KEY: &str = "dflash_route";
36pub const NAMESPACE: &str = "dflash.";
37pub const BLOCK_SLACK: usize = 256;
40const CONTEXT_CHUNK: usize = 64;
42const PENDING_ROWS: usize = 512;
45
46pub fn sidecar_path(main: &Path) -> PathBuf {
48 let name = main.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
49 if name.ends_with(".dflash.cmf") {
50 return main.to_path_buf();
51 }
52 let stem = name.strip_suffix(".cmf").unwrap_or(&name);
53 main.with_file_name(format!("{stem}.dflash.cmf"))
54}
55
56#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
58pub struct Geometry {
59 pub hidden: usize,
60 pub intermediate: usize,
61 pub layers: usize,
62 pub heads: usize,
63 pub kv_heads: usize,
64 pub head_dim: usize,
65 pub vocab: usize,
66 pub block: usize,
68 pub mask_token: u32,
69 pub conv_group: usize,
70 pub conv_kernel: usize,
71 pub selector_rank: usize,
72 pub selector_top: usize,
73 pub taps: Vec<usize>,
76 pub target_layers: usize,
77 pub window: usize,
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
82 pub windows: Vec<usize>,
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub no_rope: Vec<usize>,
86 pub rope_theta: f64,
87 pub eps: f64,
88}
89
90impl Geometry {
91 pub fn of(model: &CmfModel) -> Result<Self, String> {
93 if model.arch().arch_name != SIDECAR_ARCH {
94 return Err(format!("{} is not a DFlash sidecar", model.arch().arch_name));
95 }
96 let v = model.header.provenance.as_ref().and_then(|p| p.get("dflash")).ok_or("the sidecar's header has no dflash record")?;
97 let g: Self = serde_json::from_value(v.clone()).map_err(|e| format!("the sidecar's dflash record: {e}"))?;
98 if g.layers == 0
99 || g.block < 2
100 || g.taps.is_empty()
101 || g.kv_heads == 0
102 || g.heads % g.kv_heads != 0
103 || g.head_dim % 2 != 0
104 || g.conv_group == 0
105 || g.hidden % g.conv_group != 0
106 || !(g.windows.is_empty() || g.windows.len() == g.layers)
107 || g.no_rope.iter().any(|&l| l >= g.layers)
108 {
109 return Err(format!("unsupported DFlash geometry {g:?}"));
110 }
111 Ok(g)
112 }
113
114 pub fn tap_width(&self) -> usize {
116 self.taps.len() * self.hidden
117 }
118
119 pub fn window_of(&self, layer: usize) -> usize {
121 self.windows.get(layer).copied().unwrap_or(self.window)
122 }
123
124 pub fn rotary(&self, layer: usize) -> bool {
126 !self.no_rope.contains(&layer)
127 }
128}
129
130struct Conv {
131 base: Vec<f32>,
133 proj: Small,
134}
135
136struct Small {
140 w: Vec<f32>,
141 rows: usize,
142 cols: usize,
143}
144
145impl Small {
146 fn of(t: QTensor) -> Self {
147 let (rows, cols) = (t.rows(), t.cols());
148 let mut w = vec![0f32; rows * cols];
149 for (r, row) in w.chunks_exact_mut(cols).enumerate() {
150 t.row_f32(r, row);
151 }
152 Self { w, rows, cols }
153 }
154
155 fn apply(&self, x: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
159 let (rows, cols) = (self.rows, self.cols);
160 let mut out = vec![0f32; b * rows];
161 let Some(pool) = pool.filter(|_| b * rows * cols >= 1 << 20 && rows >= 64) else {
162 sgemm_public(b, rows, cols, 1.0, x, cols, &self.w, cols, true, &mut out, rows);
163 return out;
164 };
165 struct SendPtr(*mut f32);
166 unsafe impl Send for SendPtr {}
167 unsafe impl Sync for SendPtr {}
168 impl SendPtr {
169 fn at(&self, i: usize) -> *mut f32 {
170 unsafe { self.0.add(i) }
172 }
173 }
174 let dst = SendPtr(out.as_mut_ptr());
175 pool.run_rows(rows, &|r0, r1| {
176 let n = r1 - r0;
177 let mut part = vec![0f32; b * n];
178 sgemm_public(b, n, cols, 1.0, x, cols, &self.w[r0 * cols..r1 * cols], cols, true, &mut part, n);
179 for bi in 0..b {
180 unsafe {
183 std::ptr::copy_nonoverlapping(part[bi * n..].as_ptr(), dst.at(bi * rows + r0), n);
184 }
185 }
186 });
187 out
188 }
189}
190
191struct Layer {
192 in_norm: Vec<f32>,
193 post_norm: Vec<f32>,
194 q: QTensor,
195 k: QTensor,
196 v: QTensor,
197 o: QTensor,
198 q_norm: Vec<f32>,
199 k_norm: Vec<f32>,
200 gate: QTensor,
201 up: QTensor,
202 down: QTensor,
203 attn_conv: Conv,
204 mlp_conv: Conv,
205}
206
207#[derive(Default)]
210struct Ctx {
211 k: Vec<Vec<f32>>,
212 v: Vec<Vec<f32>>,
213 dropped: usize,
216}
217
218pub struct Drafter {
219 pub g: Geometry,
220 pub route: Option<Route>,
222 model: Arc<CmfModel>,
223 fc: QTensor,
224 hidden_norm: Vec<f32>,
225 layers: Vec<Layer>,
226 norm: Vec<f32>,
227 sel: Small,
228 books: [usize; 2],
230 inv_freq: Vec<f32>,
231 ctx: Vec<Ctx>,
232 first: usize,
235 len: usize,
236 pending: Vec<f32>,
239 device: bool,
241 fused: bool,
244 #[cfg(feature = "gpu")]
247 dev: std::sync::Mutex<Option<Result<crate::gpu_wgpu::dflash_dev::Dev, ()>>>,
248 #[cfg(feature = "cuda")]
250 cdev: std::sync::Mutex<Option<Result<crate::gpu_cuda::DflashDev, ()>>>,
251}
252
253fn vector(model: &CmfModel, name: &str, len: usize) -> Result<Vec<f32>, String> {
254 let name = format!("{NAMESPACE}{name}");
255 let e = model.tensor(&name).ok_or_else(|| format!("the sidecar has no {name}"))?;
256 if e.dtype != TensorDtype::F32 || e.shape.iter().product::<usize>() != len {
257 return Err(format!("{name}: {:?} {:?}, expected {len} f32", e.dtype, e.shape));
258 }
259 Ok(model.entry_bytes(e).chunks_exact(4).map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]])).collect())
260}
261
262static STAGE_US: [std::sync::atomic::AtomicU64; 6] = [const { std::sync::atomic::AtomicU64::new(0) }; 6];
266
267fn stage_timed() -> bool {
268 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
269 *ON.get_or_init(|| std::env::var("CMF_DFLASH_TIME").is_ok())
270}
271
272fn stage<T>(i: usize, f: impl FnOnce() -> T) -> T {
274 if !stage_timed() {
275 return f();
276 }
277 let t = std::time::Instant::now();
278 let r = f();
279 STAGE_US[i].fetch_add(t.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
280 r
281}
282
283pub fn stage_report() -> Option<String> {
285 let v: Vec<f64> = STAGE_US.iter().map(|a| a.load(std::sync::atomic::Ordering::Relaxed) as f64).collect();
286 (stage_timed() && v[5] > 0.0).then(|| {
287 let ms = |i: usize| v[i] / 1e3 / v[5];
288 format!("a block: kernel projections {:.1} ms, q k v {:.1}, attention {:.1}, o {:.1}, MLP {:.1}", ms(0), ms(1), ms(2), ms(3), ms(4))
289 })
290}
291
292#[derive(Clone, Serialize, Deserialize)]
298pub struct RouteFit {
299 pub block: SelectionDescriptor,
300 pub chain: SelectionDescriptor,
301 pub block_rows: usize,
303 pub chain_rows: usize,
304}
305
306struct Subspace {
307 mean: Vec<f32>,
308 basis: Vec<f32>,
309 rank: usize,
310}
311
312impl Subspace {
313 fn of(d: &SelectionDescriptor, hidden: usize) -> Result<Self, String> {
314 let (mean, basis) = (crate::router::decode_f16(&d.mean), crate::router::decode_f16(&d.basis));
315 match (mean, basis) {
316 (Some(mean), Some(basis)) if mean.len() == hidden && basis.len() == d.rank * hidden => Ok(Self { mean, basis, rank: d.rank }),
317 _ => Err(format!("a router subspace is not {hidden} wide at rank {}", d.rank)),
318 }
319 }
320}
321
322pub struct Route {
324 block: Subspace,
325 chain: Subspace,
326}
327
328impl Route {
329 pub fn of(fit: &RouteFit, hidden: usize) -> Result<Self, String> {
330 Ok(Self { block: Subspace::of(&fit.block, hidden)?, chain: Subspace::of(&fit.chain, hidden)? })
331 }
332
333 pub fn margin(&self, row: &[f32]) -> f32 {
336 let n = row.iter().map(|v| v * v).sum::<f32>().sqrt().max(1e-12);
337 let unit: Vec<f32> = row.iter().map(|v| v / n).collect();
338 let err = |s: &Subspace| crate::router::recon_error(&unit, &s.mean, &s.basis, s.rank);
339 err(&self.chain) - err(&self.block)
340 }
341}
342
343fn matrix(model: &Arc<CmfModel>, name: &str, rows: usize, cols: usize) -> Result<QTensor, String> {
344 let name = format!("{NAMESPACE}{name}");
345 let t = QTensor::from_model(model, &name)?;
346 if t.rows() != rows || t.cols() != cols {
347 return Err(format!("{name}: [{}, {}], expected [{rows}, {cols}]", t.rows(), t.cols()));
348 }
349 Ok(t)
350}
351
352fn rms(x: &mut [f32], w: &[f32], eps: f32) {
354 for row in x.chunks_exact_mut(w.len()) {
355 let ms = row.iter().map(|v| (*v as f64) * (*v as f64)).sum::<f64>() / w.len() as f64;
356 let s = 1.0 / (ms as f32 + eps).sqrt();
357 for (v, g) in row.iter_mut().zip(w) {
358 *v = *v * s * g;
359 }
360 }
361}
362
363impl Drafter {
364 pub fn beside(main: &Path) -> Option<Result<Self, String>> {
366 let p = sidecar_path(main);
367 p.exists().then(|| CmfModel::open(&p).map_err(|e| format!("{}: {e}", p.display())).and_then(|m| Self::new(Arc::new(m))))
368 }
369
370 pub fn for_target(main: &CmfModel) -> Option<Result<Self, String>> {
373 let r = Self::beside(&main.path)?;
374 Some(r.and_then(|d| {
375 let a = main.arch();
376 if a.num_layers != d.g.target_layers || a.hidden_size != d.g.hidden || a.vocab_size != d.g.vocab {
377 return Err(format!("it drafts for {} layers of {} over {} tokens", d.g.target_layers, d.g.hidden, d.g.vocab));
378 }
379 let want = d.model.header.provenance.as_ref().and_then(|p| p["base_binding"]["trunk_hash64"].as_str().map(str::to_string));
380 let have = format!("{:016x}", main.trunk_hash());
381 if want.as_deref() != Some(have.as_str()) {
382 return Err(format!("it was converted for the trunk {}, this one is {have}", want.as_deref().unwrap_or("?")));
383 }
384 Ok(d)
385 }))
386 }
387
388 pub fn new(model: Arc<CmfModel>) -> Result<Self, String> {
389 let g = Geometry::of(&model)?;
390 let (hs, ffn, qd, kd) = (g.hidden, g.intermediate, g.heads * g.head_dim, g.kv_heads * g.head_dim);
391 let conv = |p: &str| -> Result<Conv, String> {
392 Ok(Conv {
393 base: vector(&model, &format!("{p}.base_kernel"), 2 * g.conv_kernel * hs)?,
394 proj: Small::of(matrix(&model, &format!("{p}.kernel_projection.weight"), 2 * g.conv_kernel * (hs / g.conv_group), hs)?),
395 })
396 };
397 let layers = (0..g.layers)
398 .map(|i| {
399 let p = format!("layers.{i}");
400 Ok(Layer {
401 in_norm: vector(&model, &format!("{p}.input_layernorm.weight"), hs)?,
402 post_norm: vector(&model, &format!("{p}.post_attention_layernorm.weight"), hs)?,
403 q: matrix(&model, &format!("{p}.self_attn.q_proj.weight"), qd, hs)?,
404 k: matrix(&model, &format!("{p}.self_attn.k_proj.weight"), kd, hs)?,
405 v: matrix(&model, &format!("{p}.self_attn.v_proj.weight"), kd, hs)?,
406 o: matrix(&model, &format!("{p}.self_attn.o_proj.weight"), hs, qd)?,
407 q_norm: vector(&model, &format!("{p}.self_attn.q_norm.weight"), g.head_dim)?,
408 k_norm: vector(&model, &format!("{p}.self_attn.k_norm.weight"), g.head_dim)?,
409 gate: matrix(&model, &format!("{p}.mlp.gate_proj.weight"), ffn, hs)?,
410 up: matrix(&model, &format!("{p}.mlp.up_proj.weight"), ffn, hs)?,
411 down: matrix(&model, &format!("{p}.mlp.down_proj.weight"), hs, ffn)?,
412 attn_conv: conv(&format!("{p}.attention_conv"))?,
413 mlp_conv: conv(&format!("{p}.mlp_conv"))?,
414 })
415 })
416 .collect::<Result<Vec<_>, String>>()?;
417 let book = |name: &str| -> Result<usize, String> {
418 let name = format!("{NAMESPACE}candidate_selector.{name}");
419 let i = model.tensor_index(&name).ok_or_else(|| format!("the sidecar has no {name}"))?;
420 let e = &model.tensors[i];
421 if e.dtype != TensorDtype::F16 || e.shape != [g.vocab, g.selector_rank] {
422 return Err(format!("{name}: {:?} {:?}, expected f16 [{}, {}]", e.dtype, e.shape, g.vocab, g.selector_rank));
423 }
424 Ok(i)
425 };
426 let half = g.head_dim / 2;
427 let route = model
429 .header
430 .provenance
431 .as_ref()
432 .and_then(|p| p.get(ROUTE_KEY))
433 .and_then(|v| serde_json::from_value::<RouteFit>(v.clone()).ok())
434 .and_then(|fit| Route::of(&fit, hs).map_err(|e| tracing::warn!("DFlash sidecar: {e}")).ok());
435 Ok(Self {
436 route,
437 fc: matrix(&model, "fc.weight", hs, g.tap_width())?,
438 hidden_norm: vector(&model, "hidden_norm.weight", hs)?,
439 norm: vector(&model, "norm.weight", hs)?,
440 sel: Small::of(matrix(&model, "candidate_selector.hidden_projection.weight", g.selector_rank, hs)?),
441 books: [book("predecessor_codebook")?, book("successor_codebook")?],
442 inv_freq: (0..half).map(|i| (g.rope_theta.powf(-(2.0 * i as f64) / g.head_dim as f64)) as f32).collect(),
443 ctx: (0..g.layers).map(|_| Ctx { k: vec![Vec::new(); g.kv_heads], v: vec![Vec::new(); g.kv_heads], dropped: 0 }).collect(),
444 first: 0,
445 len: 0,
446 pending: Vec::new(),
447 device: crate::gpu::enabled(),
448 fused: std::env::var("CMF_DFLASH_FUSED").as_deref() != Ok("0"),
449 #[cfg(feature = "gpu")]
450 dev: std::sync::Mutex::new(None),
451 #[cfg(feature = "cuda")]
452 cdev: std::sync::Mutex::new(None),
453 layers,
454 model,
455 g,
456 })
457 }
458
459 #[cfg(feature = "gpu")]
465 pub fn dev_ready(&self) -> bool {
466 #[cfg(feature = "cuda")]
467 if crate::gpu::cuda_active() {
468 return self.cuda_ready();
469 }
470 use crate::gpu_wgpu::dflash_dev::{Dev, LayerSpec, Spec};
471 let mut slot = self.dev.lock().unwrap();
472 if slot.is_none() {
473 if !(self.device && crate::gpu_wgpu::selected() && crate::gpu_wgpu::enabled()) {
476 return false;
477 }
478 let built = if crate::gpu_wgpu::dflash_dev::wanted() {
479 let g = &self.g;
480 let idx = |name: &str| self.model.tensor_index(&format!("{NAMESPACE}{name}"));
481 let layers: Option<Vec<LayerSpec>> = self
482 .layers
483 .iter()
484 .enumerate()
485 .map(|(i, l)| {
486 let p = format!("layers.{i}");
487 Some(LayerSpec {
488 in_norm: l.in_norm.clone(),
489 post_norm: l.post_norm.clone(),
490 q_norm: l.q_norm.clone(),
491 k_norm: l.k_norm.clone(),
492 attn_base: l.attn_conv.base.clone(),
493 mlp_base: l.mlp_conv.base.clone(),
494 window: g.window_of(i),
495 rotary: g.rotary(i),
496 q: self.tiled(&l.q)?,
497 k: self.tiled(&l.k)?,
498 v: self.tiled(&l.v)?,
499 o: self.tiled(&l.o)?,
500 gate: self.tiled(&l.gate)?,
501 up: self.tiled(&l.up)?,
502 down: self.tiled(&l.down)?,
503 attn_proj: idx(&format!("{p}.attention_conv.kernel_projection.weight"))?,
504 mlp_proj: idx(&format!("{p}.mlp_conv.kernel_projection.weight"))?,
505 })
506 })
507 .collect();
508 let spec = layers.and_then(|layers| {
509 Some(Spec {
510 hs: g.hidden,
511 ffn: g.intermediate,
512 hd: g.head_dim,
513 nh: g.heads,
514 kv: g.kv_heads,
515 kn: g.conv_kernel,
516 gs: g.conv_group,
517 block: g.block,
518 eps: g.eps as f32,
519 layers,
520 norm: self.norm.clone(),
521 inv_freq: self.inv_freq.clone(),
522 sel: idx("candidate_selector.hidden_projection.weight")?,
523 sel_rank: g.selector_rank,
524 })
525 });
526 match spec {
527 Some(spec) => match Dev::new(self.model.clone(), spec) {
528 Ok(mut dev) => {
529 for li in 0..self.layers.len() {
530 self.dev_refill(&mut dev, li);
531 }
532 tracing::info!("DFlash drafter: the block runs on the device as one submission");
533 Ok(dev)
534 }
535 Err(e) => {
536 tracing::info!("DFlash drafter: the block stays on the host ({e})");
537 Err(())
538 }
539 },
540 None => Err(()),
541 }
542 } else {
543 Err(())
544 };
545 *slot = Some(built);
546 }
547 matches!(slot.as_ref(), Some(Ok(_)))
548 }
549
550 #[cfg(feature = "cuda")]
554 fn cuda_ready(&self) -> bool {
555 let mut slot = self.cdev.lock().unwrap();
556 if slot.is_none() {
557 if !self.device || std::env::var("CMF_DFLASH_DEV").as_deref() == Ok("0") {
558 *slot = Some(Err(()));
559 } else if !crate::gpu_cuda::hybrid_kernels_ready() {
560 return false;
562 } else {
563 let t0 = std::time::Instant::now();
564 let built = self.cuda_build();
565 match built {
566 Ok(mut dev) => {
567 let (hd, kv) = (self.g.head_dim, self.g.kv_heads);
568 for (li, c) in self.ctx.iter().enumerate() {
569 let rows = c.k.first().map(|h| h.len() / hd).unwrap_or(0);
570 if rows == 0 {
571 continue;
572 }
573 let (mut k, mut v) = (vec![0f32; rows * kv * hd], vec![0f32; rows * kv * hd]);
574 for r in 0..rows {
575 for h in 0..kv {
576 k[(r * kv + h) * hd..][..hd].copy_from_slice(&c.k[h][r * hd..][..hd]);
577 v[(r * kv + h) * hd..][..hd].copy_from_slice(&c.v[h][r * hd..][..hd]);
578 }
579 }
580 let _ = dev.push_rows(li, c.dropped, &k, &v);
581 }
582 tracing::info!("DFlash drafter: the block runs on the CUDA card (built in {:.1} s)", t0.elapsed().as_secs_f64());
583 *slot = Some(Ok(dev));
584 }
585 Err(e) => {
586 tracing::info!("DFlash drafter: the block stays on the host ({e})");
587 *slot = Some(Err(()));
588 }
589 }
590 }
591 }
592 matches!(slot.as_ref(), Some(Ok(_)))
593 }
594
595 #[cfg(feature = "cuda")]
596 fn cuda_build(&self) -> Result<crate::gpu_cuda::DflashDev, String> {
597 use crate::gpu::{GraphPrismOp, GraphW};
598 fn gw(t: &QTensor) -> Result<GraphW<'_>, String> {
599 let (_, idx, kind, rs) = t.graph_weight().ok_or("a matrix without a graph weight")?;
600 Ok(GraphW { idx, kind, row_scale: rs, data: &[], prism: GraphPrismOp::None, affine: false })
601 }
602 fn dense(t: &QTensor) -> Vec<f32> {
604 let (rows, cols) = (t.rows(), t.cols());
605 let mut w = vec![0f32; rows * cols];
606 let threads = std::thread::available_parallelism().map_or(4, |n| n.get()).clamp(1, 16);
607 let per = rows.div_ceil(threads);
608 std::thread::scope(|sc| {
609 for (ci, chunk) in w.chunks_mut(per * cols).enumerate() {
610 sc.spawn(move || {
611 for (r, row) in chunk.chunks_exact_mut(cols).enumerate() {
612 t.row_f32(ci * per + r, row);
613 }
614 });
615 }
616 });
617 w
618 }
619 let g = &self.g;
620 let layers = self
621 .layers
622 .iter()
623 .enumerate()
624 .map(|(i, l)| {
625 Ok(crate::gpu_cuda::DflashLayerIn {
626 in_norm: &l.in_norm,
627 post_norm: &l.post_norm,
628 q_norm: &l.q_norm,
629 k_norm: &l.k_norm,
630 attn_base: &l.attn_conv.base,
631 mlp_base: &l.mlp_conv.base,
632 attn_proj: &l.attn_conv.proj.w,
633 mlp_proj: &l.mlp_conv.proj.w,
634 window: g.window_of(i),
635 rotary: g.rotary(i),
636 q: gw(&l.q)?,
637 k: gw(&l.k)?,
638 v: gw(&l.v)?,
639 o: gw(&l.o)?,
640 gate: gw(&l.gate)?,
641 up: gw(&l.up)?,
642 down: gw(&l.down)?,
643 })
644 })
645 .collect::<Result<Vec<_>, String>>()?;
646 let t0 = std::time::Instant::now();
647 let fc = dense(&self.fc);
648 tracing::debug!("DFlash drafter: fc to f32 in {:.0} ms", t0.elapsed().as_secs_f64() * 1e3);
649 let spec = crate::gpu_cuda::DflashSpecIn {
650 hs: g.hidden,
651 ffn: g.intermediate,
652 hd: g.head_dim,
653 nh: g.heads,
654 kv: g.kv_heads,
655 kn: g.conv_kernel,
656 gs: g.conv_group,
657 block: g.block,
658 eps: g.eps as f32,
659 sel_rank: g.selector_rank,
660 tw: g.tap_width(),
661 layers,
662 norm: &self.norm,
663 hidden_norm: &self.hidden_norm,
664 inv_freq: &self.inv_freq,
665 fc: &fc,
666 sel: &self.sel.w,
667 };
668 crate::gpu_cuda::DflashDev::new(&self.model, &spec)
669 }
670
671 #[cfg(feature = "gpu")]
673 fn dev_refill(&self, dev: &mut crate::gpu_wgpu::dflash_dev::Dev, layer: usize) {
674 let hd = self.g.head_dim;
675 let c = &self.ctx[layer];
676 let rows = c.k.first().map(|h| h.len() / hd).unwrap_or(0);
677 let _ = dev.reserve(layer, c.dropped + rows);
679 self.dev_refill_held(dev, layer);
680 }
681
682 #[cfg(feature = "gpu")]
685 fn dev_refill_held(&self, dev: &mut crate::gpu_wgpu::dflash_dev::Dev, layer: usize) {
686 let (hd, kv) = (self.g.head_dim, self.g.kv_heads);
687 let c = &self.ctx[layer];
688 let rows = c.k.first().map(|h| h.len() / hd).unwrap_or(0);
689 if rows == 0 {
690 return;
691 }
692 let mut k = vec![0f32; rows * kv * hd];
693 let mut v = vec![0f32; rows * kv * hd];
694 for r in 0..rows {
695 for h in 0..kv {
696 k[(r * kv + h) * hd..][..hd].copy_from_slice(&c.k[h][r * hd..][..hd]);
697 v[(r * kv + h) * hd..][..hd].copy_from_slice(&c.v[h][r * hd..][..hd]);
698 }
699 }
700 dev.push_rows(layer, c.dropped, &k, &v);
701 }
702
703 pub fn host_only(&mut self) {
705 self.device = false;
706 }
707
708 pub fn next_position(&self) -> usize {
710 self.first + self.len + self.pending.len() / self.g.tap_width()
711 }
712
713 pub fn span(&self) -> (usize, usize) {
716 (self.first, self.next_position())
717 }
718
719 pub fn reset(&mut self) {
720 self.truncate(0);
721 self.first = 0;
722 }
723
724 pub fn truncate(&mut self, pos: usize) {
726 let held = self.first + self.len;
727 self.pending.truncate(pos.saturating_sub(held) * self.g.tap_width());
728 let keep = pos.saturating_sub(self.first).min(self.len);
729 let hd = self.g.head_dim;
730 for c in &mut self.ctx {
731 let rows = keep.saturating_sub(c.dropped);
732 for h in c.k.iter_mut().chain(c.v.iter_mut()) {
733 h.truncate(rows * hd);
734 }
735 c.dropped = c.dropped.min(keep);
736 }
737 self.len = keep;
738 }
739
740 pub fn context(&self, layer: usize, kv_head: usize) -> (&[f32], &[f32]) {
743 (&self.ctx[layer].k[kv_head], &self.ctx[layer].v[kv_head])
744 }
745
746 fn mm(&self, w: &QTensor, x: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
747 let mut out = vec![0f32; b * w.rows()];
748 if !(self.device && w.device_matmat(x, b, &mut out)) {
749 w.matmat(x, b, &mut out, pool);
750 }
751 out
752 }
753
754
755 fn tiled(&self, w: &QTensor) -> Option<usize> {
757 w.mapped_q4tp().filter(|(m, _)| Arc::ptr_eq(m, &self.model)).map(|(_, i)| i)
758 }
759
760 fn many(&self, ws: &[&QTensor], x: &[f32], b: usize, pool: Option<&Pool>) -> Vec<Vec<f32>> {
763 let mut outs: Vec<Vec<f32>> = ws.iter().map(|w| vec![0f32; b * w.rows()]).collect();
764 if self.device && self.fused {
765 let jobs: Option<Vec<(usize, usize, usize, usize)>> = ws.iter().map(|w| self.tiled(w).map(|i| (i, w.rows(), w.cols(), w.rows()))).collect();
766 if let Some(jobs) = jobs {
767 let mut refs: Vec<&mut [f32]> = outs.iter_mut().map(|o| o.as_mut_slice()).collect();
768 if b <= 8 && crate::gpu::q4tp_rows8_many(&self.model, &jobs, x, b, &mut refs) {
770 return outs;
771 }
772 let wide: Vec<(usize, usize, usize)> = jobs.iter().map(|j| (j.0, j.1, j.2)).collect();
773 if crate::gpu::q4tp_matmat_many(&self.model, &wide, x, b, &mut refs) {
774 return outs;
775 }
776 }
777 }
778 for (w, o) in ws.iter().zip(outs.iter_mut()) {
779 if !(self.device && w.device_matmat(x, b, o)) {
780 w.matmat(x, b, o, pool);
781 }
782 }
783 outs
784 }
785
786 fn mlp(&self, l: &Layer, x: &[f32], b: usize, pool: Option<&Pool>) -> Vec<f32> {
789 if self.device && self.fused {
790 if let (Some(g), Some(u), Some(d)) = (self.tiled(&l.gate), self.tiled(&l.up), self.tiled(&l.down)) {
791 let (hs, ffn) = (self.g.hidden, self.g.intermediate);
792 let mut out = vec![0f32; b * hs];
793 if (b <= 8 && crate::gpu::q4tp_rows8_ffn(&self.model, g, u, d, x, b, hs, ffn, &mut out))
794 || crate::gpu::q4tp_ffn(&self.model, g, u, d, x, b, hs, ffn, &mut out)
795 {
796 return out;
797 }
798 }
799 }
800 let mut gu = self.many(&[&l.gate, &l.up], x, b, pool);
801 let u = gu.pop().unwrap();
802 let mut a = gu.pop().unwrap();
803 for (a, u) in a.iter_mut().zip(&u) {
804 *a = *a / (1.0 + (-*a).exp()) * u;
805 }
806 self.mm(&l.down, &a, b, pool)
807 }
808
809 fn rotate(&self, x: &mut [f32], heads: usize, pos: impl Fn(usize) -> usize) {
812 let (hd, half) = (self.g.head_dim, self.g.head_dim / 2);
813 let mut cs = vec![(0f32, 0f32); half];
814 for (r, row) in x.chunks_exact_mut(heads * hd).enumerate() {
815 let p = pos(r) as f32;
816 for (c, f) in cs.iter_mut().zip(&self.inv_freq) {
817 let a = p * f;
818 *c = (a.cos(), a.sin());
819 }
820 for h in row.chunks_exact_mut(hd) {
821 let (lo, hi) = h.split_at_mut(half);
822 for i in 0..half {
823 let (c, s) = cs[i];
824 let (a, b) = (lo[i], hi[i]);
825 lo[i] = a * c - b * s;
826 hi[i] = b * c + a * s;
827 }
828 }
829 }
830 }
831
832 pub fn push_context(&mut self, pos: usize, taps: &[f32], pool: Option<&Pool>) {
837 self.queue_context(pos, taps, pool);
838 self.settle(pool);
839 }
840
841 pub fn queue_context(&mut self, pos: usize, taps: &[f32], pool: Option<&Pool>) {
846 let tw = self.g.tap_width();
847 assert!(taps.len() % tw == 0, "dflash: a context row is {tw} wide");
848 if pos > self.next_position() || pos < self.first {
849 self.truncate(0);
850 self.first = pos;
851 } else {
852 self.truncate(pos);
853 }
854 self.pending.extend_from_slice(taps);
855 let held = self.pending.len() / tw;
858 let limit = if crate::gpu::cuda_active() && (0..self.g.layers).all(|l| self.g.window_of(l) > 0) {
861 PENDING_ROWS.max((0..self.g.layers).map(|l| self.g.window_of(l)).max().unwrap_or(0) + BLOCK_SLACK)
862 } else {
863 PENDING_ROWS
864 };
865 if held > limit {
866 let over = (held - limit).next_multiple_of(CONTEXT_CHUNK).min(held);
867 let rows: Vec<f32> = self.pending.drain(..over * tw).collect();
868 self.settle_rows(self.first + self.len, &rows, pool);
869 }
870 }
871
872 pub fn settle(&mut self, pool: Option<&Pool>) {
874 if self.pending.is_empty() {
875 return;
876 }
877 let rows = std::mem::take(&mut self.pending);
878 self.settle_rows(self.first + self.len, &rows, pool);
879 }
880
881 fn settle_rows(&mut self, pos: usize, taps: &[f32], pool: Option<&Pool>) {
882 let t_settle = std::time::Instant::now();
883 let (hs, tw, hd, kv) = (self.g.hidden, self.g.tap_width(), self.g.head_dim, self.g.kv_heads);
884 let eps = self.g.eps as f32;
885 #[cfg(feature = "cuda")]
887 let per = if crate::gpu::cuda_active() { crate::gpu_cuda::DFLASH_SETTLE_MAX } else { CONTEXT_CHUNK };
888 #[cfg(not(feature = "cuda"))]
889 let per = CONTEXT_CHUNK;
890 for (ci, chunk) in taps.chunks(per * tw).enumerate() {
891 let (n, at) = (chunk.len() / tw, pos + ci * per);
892 #[cfg(feature = "cuda")]
894 if crate::gpu::cuda_active() && self.cuda_ready() {
895 let rows = match self.cdev.lock().unwrap().as_mut() {
896 Some(Ok(dev)) => dev.settle(at - self.first, at, chunk, n).map_err(|e| tracing::warn!("DFlash context on the card: {e}")).ok(),
897 _ => None,
898 };
899 if let Some(rows) = rows {
900 for (li, (k, v)) in rows.into_iter().enumerate() {
901 let c = &mut self.ctx[li];
902 for r in 0..n {
903 for h in 0..kv {
904 let o = (r * kv + h) * hd;
905 c.k[h].extend_from_slice(&k[o..o + hd]);
906 c.v[h].extend_from_slice(&v[o..o + hd]);
907 }
908 }
909 }
910 self.len += n;
911 continue;
912 }
913 }
914 let mut t = self.mm(&self.fc, chunk, n, pool);
915 rms(&mut t, &self.hidden_norm, eps);
916 debug_assert_eq!(t.len(), n * hs);
917 let ws: Vec<&QTensor> = self.layers.iter().flat_map(|l| [&l.k, &l.v]).collect();
918 let mut kvs = self.many(&ws, &t, n, pool).into_iter();
919 for li in 0..self.layers.len() {
920 let l = &self.layers[li];
921 let (mut k, v) = (kvs.next().unwrap(), kvs.next().unwrap());
922 rms(&mut k, &l.k_norm, eps);
923 if self.g.rotary(li) {
924 self.rotate(&mut k, kv, |r| at + r);
925 }
926 #[cfg(feature = "gpu")]
927 if let Some(Ok(dev)) = self.dev.lock().unwrap().as_mut() {
928 let at = at - self.first;
929 if dev.reserve(li, at + n) {
930 dev.push_rows(li, at, &k, &v);
931 } else {
932 self.dev_refill_held(dev, li);
934 dev.push_rows(li, at, &k, &v);
935 }
936 }
937 let c = &mut self.ctx[li];
938 for r in 0..n {
939 for h in 0..kv {
940 let o = (r * kv + h) * hd;
941 c.k[h].extend_from_slice(&k[o..o + hd]);
942 c.v[h].extend_from_slice(&v[o..o + hd]);
943 }
944 }
945 }
946 self.len += n;
947 }
948 if std::env::var_os("CMF_DFLASH_TIME").is_some() {
949 eprintln!("dflash context: {} rows in {:.1} ms", taps.len() / tw, t_settle.elapsed().as_secs_f64() * 1e3);
950 }
951 for (li, c) in self.ctx.iter_mut().enumerate() {
953 let (w, held) = (self.g.window_of(li), self.len - c.dropped);
954 if w > 0 && held > 2 * w {
955 let cut = held - w;
956 for h in c.k.iter_mut().chain(c.v.iter_mut()) {
957 h.drain(..cut * hd);
958 }
959 c.dropped += cut;
960 }
961 }
962 }
963
964 fn conv_in(&self, c: &Conv, x: &[f32], rows: usize, pool: Option<&Pool>) -> (Vec<f32>, Vec<f32>) {
967 let d = stage(0, || c.proj.apply(x, rows, pool));
968 (self.conv(c, 0, x, &d, rows), d)
969 }
970
971 fn conv(&self, c: &Conv, stage: usize, x: &[f32], d: &[f32], rows: usize) -> Vec<f32> {
973 let (hs, gs, kn) = (self.g.hidden, self.g.conv_group, self.g.conv_kernel);
974 let groups = hs / gs;
975 let mut y = vec![0f32; rows * hs];
976 for r in 0..rows {
977 let out = &mut y[r * hs..(r + 1) * hs];
978 for o in 0..kn.min(r + 1) {
979 let src = &x[(r - o) * hs..(r - o + 1) * hs];
980 let base = &c.base[(stage * kn + o) * hs..(stage * kn + o + 1) * hs];
981 let dyn_ = &d[r * 2 * kn * groups + (stage * kn + o) * groups..][..groups];
982 for g in 0..groups {
983 let k = dyn_[g];
984 for i in g * gs..(g + 1) * gs {
985 out[i] += (base[i] + k) * src[i];
986 }
987 }
988 }
989 }
990 y
991 }
992
993 pub fn block(&self, anchor_pos: usize, mut h: Vec<f32>, pool: Option<&Pool>) -> Vec<f32> {
997 let g = &self.g;
998 let (hs, hd, nh, kv, rows) = (g.hidden, g.head_dim, g.heads, g.kv_heads, g.block);
999 assert_eq!(h.len(), rows * hs, "dflash: a block is {rows} rows");
1000 debug_assert!(self.pending.is_empty(), "dflash: rows wait to become context (settle)");
1001 let (rep, eps, scale) = (nh / kv, g.eps as f32, (hd as f32).powf(-0.5));
1002 let m = rep * rows;
1003 let (mut qg, mut sc, mut og) = (vec![0f32; m * hd], Vec::new(), vec![0f32; m * hd]);
1004 let (mut kb, mut vb) = (vec![0f32; rows * hd], vec![0f32; rows * hd]);
1005 for (li, l) in self.layers.iter().enumerate() {
1006 let (c, window) = (&self.ctx[li], g.window_of(li));
1008 let base = self.first + c.dropped;
1009 let hi = anchor_pos.min(self.first + self.len).max(base);
1010 let lo = if window > 0 { (anchor_pos + 1).saturating_sub(window).clamp(base, hi) } else { base };
1011 let n = hi - lo;
1012 let (clo, chi) = ((lo - base) * hd, (hi - base) * hd);
1013 sc.clear();
1014 sc.resize(m * (n + rows), 0f32);
1015 let mut x = h.clone();
1016 rms(&mut x, &l.in_norm, eps);
1017 let (x, ad) = self.conv_in(&l.attn_conv, &x, rows, pool);
1018 let mut qkv = stage(1, || self.many(&[&l.q, &l.k, &l.v], &x, rows, pool));
1019 let t_attn = stage_timed().then(std::time::Instant::now);
1020 let v = qkv.pop().unwrap();
1021 let mut k = qkv.pop().unwrap();
1022 let mut q = qkv.pop().unwrap();
1023 rms(&mut q, &l.q_norm, eps);
1024 rms(&mut k, &l.k_norm, eps);
1025 if g.rotary(li) {
1026 self.rotate(&mut q, nh, |r| anchor_pos + r);
1027 self.rotate(&mut k, kv, |r| anchor_pos + r);
1028 }
1029 let mut y = vec![0f32; rows * nh * hd];
1030 for gi in 0..kv {
1031 for qi in 0..rep {
1032 for r in 0..rows {
1033 let src = (r * nh + gi * rep + qi) * hd;
1034 qg[(qi * rows + r) * hd..][..hd].copy_from_slice(&q[src..src + hd]);
1035 }
1036 }
1037 for r in 0..rows {
1038 kb[r * hd..][..hd].copy_from_slice(&k[(r * kv + gi) * hd..][..hd]);
1039 vb[r * hd..][..hd].copy_from_slice(&v[(r * kv + gi) * hd..][..hd]);
1040 }
1041 let w = n + rows;
1043 if n > 0 {
1044 sgemm_public(m, n, hd, scale, &qg, hd, &c.k[gi][clo..chi], hd, true, &mut sc, w);
1045 }
1046 sgemm_public(m, rows, hd, scale, &qg, hd, &kb, hd, true, &mut sc[n..], w);
1047 for i in 0..m {
1048 let row = &mut sc[i * w..(i + 1) * w];
1049 if window > 0 {
1051 let from = (anchor_pos + i % rows + 1).saturating_sub(window);
1052 row[..from.saturating_sub(lo).min(n)].fill(f32::NEG_INFINITY);
1053 }
1054 let top = row.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1055 let mut sum = 0f32;
1056 for s in row.iter_mut() {
1057 *s = (*s - top).exp();
1058 sum += *s;
1059 }
1060 row.iter_mut().for_each(|s| *s /= sum);
1061 }
1062 sgemm_public(m, hd, rows, 1.0, &sc[n..], w, &vb, hd, false, &mut og, hd);
1063 if n > 0 {
1064 let mut oc = vec![0f32; m * hd];
1065 sgemm_public(m, hd, n, 1.0, &sc, w, &c.v[gi][clo..chi], hd, false, &mut oc, hd);
1066 og.iter_mut().zip(&oc).for_each(|(a, b)| *a += b);
1067 }
1068 for qi in 0..rep {
1069 for r in 0..rows {
1070 let dst = (r * nh + gi * rep + qi) * hd;
1071 y[dst..dst + hd].copy_from_slice(&og[(qi * rows + r) * hd..][..hd]);
1072 }
1073 }
1074 }
1075 if let Some(t) = t_attn {
1076 STAGE_US[2].fetch_add(t.elapsed().as_micros() as u64, std::sync::atomic::Ordering::Relaxed);
1077 }
1078 let o = stage(3, || self.many(&[&l.o], &y, rows, pool).pop().unwrap());
1079 let o = self.conv(&l.attn_conv, 1, &o, &ad, rows);
1080 h.iter_mut().zip(&o).for_each(|(a, b)| *a += b);
1081
1082 let mut x = h.clone();
1083 rms(&mut x, &l.post_norm, eps);
1084 let (x, md) = self.conv_in(&l.mlp_conv, &x, rows, pool);
1085 let d = stage(4, || self.mlp(l, &x, rows, pool));
1086 let d = self.conv(&l.mlp_conv, 1, &d, &md, rows);
1087 h.iter_mut().zip(&d).for_each(|(a, b)| *a += b);
1088 }
1089 rms(&mut h, &self.norm, eps);
1090 if stage_timed() {
1091 STAGE_US[5].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1092 }
1093 h
1094 }
1095
1096 #[cfg(feature = "gpu")]
1101 pub fn block_dev(&self, anchor_pos: usize, h: &[f32], k: usize, head: &QTensor) -> Option<(Vec<f32>, Vec<f32>)> {
1102 use crate::gpu_wgpu::dflash_dev::{CtxRange, kind_of};
1103 if k == 0 || k >= self.g.block || !self.dev_ready() {
1104 return None;
1105 }
1106 #[cfg(feature = "cuda")]
1109 if crate::gpu::cuda_active() {
1110 let (hmodel, hidx, hkind, hrs) = head.graph_weight()?;
1111 let hgw = crate::gpu::GraphW { idx: hidx, kind: hkind, row_scale: hrs, data: &[], prism: crate::gpu::GraphPrismOp::None, affine: false };
1112 let width = std::env::var("CMF_DFLASH_VOCAB").ok().and_then(|v| v.parse::<usize>().ok()).unwrap_or(65536).clamp(self.g.selector_top, head.rows());
1113 let g = &self.g;
1114 let ranges: Vec<(usize, usize, usize)> = (0..self.layers.len())
1115 .map(|li| {
1116 let (c, window) = (&self.ctx[li], g.window_of(li));
1117 let base = self.first + c.dropped;
1118 let hi = anchor_pos.min(self.first + self.len).max(base);
1119 let lo = if window > 0 { (anchor_pos + 1).saturating_sub(window).clamp(base, hi) } else { base };
1120 (lo - self.first, hi - lo, lo)
1121 })
1122 .collect();
1123 let mut slot = self.cdev.lock().unwrap();
1124 let Some(Ok(dev)) = slot.as_mut() else { return None };
1125 return match dev.block(anchor_pos, h, &ranges, hmodel, &hgw, head.rows(), width, k) {
1126 Ok(out) => {
1127 if stage_timed() {
1128 STAGE_US[5].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1129 }
1130 Some(out)
1131 }
1132 Err(e) => {
1133 tracing::warn!("DFlash block on the card: {e}");
1134 None
1135 }
1136 };
1137 }
1138 let (hmodel, hidx) = head.mapped_device_gemm()?;
1139 let kind = kind_of(hmodel, hidx)?;
1140 let g = &self.g;
1141 let ranges: Vec<CtxRange> = (0..self.layers.len())
1142 .map(|li| {
1143 let (c, window) = (&self.ctx[li], g.window_of(li));
1144 let base = self.first + c.dropped;
1145 let hi = anchor_pos.min(self.first + self.len).max(base);
1146 let lo = if window > 0 { (anchor_pos + 1).saturating_sub(window).clamp(base, hi) } else { base };
1147 CtxRange { lo: lo - self.first, n: hi - lo }
1148 })
1149 .collect();
1150 let mut slot = self.dev.lock().unwrap();
1151 let Some(Ok(dev)) = slot.as_mut() else { return None };
1152 let out = dev.block(anchor_pos, self.first, h, &ranges, (hmodel, hidx, head.rows(), head.cols(), kind), head.rows(), k)?;
1153 if stage_timed() {
1154 STAGE_US[5].fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1155 }
1156 Some((out.logits, out.hp))
1157 }
1158
1159 fn book_row(&self, book: usize, token: u32, dst: &mut [f32]) {
1160 let e = &self.model.tensors[self.books[book]];
1161 let rank = self.g.selector_rank;
1162 let bytes = &self.model.entry_bytes(e)[token as usize * rank * 2..][..rank * 2];
1163 for (d, b) in dst.iter_mut().zip(bytes.chunks_exact(2)) {
1164 *d = f16_to_f32(u16::from_le_bytes([b[0], b[1]]));
1165 }
1166 }
1167
1168 pub fn select(&self, hidden: &[f32], logits: &[f32], anchor: u32) -> (Vec<u32>, Vec<u32>) {
1175 let n = hidden.len() / self.g.hidden;
1176 let hp = self.sel.apply(hidden, n, None);
1177 self.select_with(&hp, logits, anchor, n)
1178 }
1179
1180 pub fn select_with(&self, hp: &[f32], logits: &[f32], anchor: u32, n: usize) -> (Vec<u32>, Vec<u32>) {
1183 let (rank, top, vocab) = (self.g.selector_rank, self.g.selector_top, self.g.vocab);
1184 let width = logits.len() / n.max(1);
1185 assert!(logits.len() == n * width && (top..=vocab).contains(&width), "dflash: logits of {n} rows over the head's rows");
1186 assert!(hp.len() >= n * rank, "dflash: the selector's rows");
1187 let (mut prev, mut chosen, mut plain) = (anchor, Vec::with_capacity(n), Vec::with_capacity(n));
1188 let (mut pc, mut sc) = (vec![0f32; rank], vec![0f32; rank]);
1189 for j in 0..n {
1190 let cand = top_k(&logits[j * width..(j + 1) * width], top);
1191 plain.push(cand[0].1);
1192 self.book_row(0, prev, &mut pc);
1193 pc.iter_mut().zip(&hp[j * rank..(j + 1) * rank]).for_each(|(p, h)| *p *= h);
1194 let mut best = (f32::NEG_INFINITY, cand[0].1);
1195 for &(unary, tok) in &cand {
1196 self.book_row(1, tok, &mut sc);
1197 let s = unary + pc.iter().zip(&sc).map(|(a, b)| a * b).sum::<f32>();
1198 if s > best.0 {
1199 best = (s, tok);
1200 }
1201 }
1202 prev = best.1;
1203 chosen.push(prev);
1204 }
1205 (chosen, plain)
1206 }
1207}
1208
1209pub fn top_k(x: &[f32], k: usize) -> Vec<(f32, u32)> {
1211 let mut best: Vec<(f32, u32)> = Vec::with_capacity(k + 1);
1212 for (i, &v) in x.iter().enumerate() {
1213 if best.len() == k && v <= best[k - 1].0 {
1214 continue;
1215 }
1216 let at = best.partition_point(|b| b.0 >= v);
1217 best.insert(at, (v, i as u32));
1218 best.truncate(k);
1219 }
1220 best
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225 use super::*;
1226
1227 #[test]
1228 fn top_k_orders_and_keeps_first_of_equals() {
1229 let x = [0.5, 2.0, -1.0, 2.0, 3.5, 0.0];
1230 assert_eq!(top_k(&x, 3), vec![(3.5, 4), (2.0, 1), (2.0, 3)]);
1231 assert_eq!(top_k(&x, 1), vec![(3.5, 4)]);
1232 }
1233
1234 #[test]
1235 fn sidecar_name() {
1236 assert_eq!(sidecar_path(Path::new("/m/a.cmf")), PathBuf::from("/m/a.dflash.cmf"));
1237 assert_eq!(sidecar_path(Path::new("/m/a.dflash.cmf")), PathBuf::from("/m/a.dflash.cmf"));
1238 }
1239}