use candle_core::{DType, Device, Module, Result as CandleResult, Tensor};
use candle_nn::VarBuilder;
use candle_transformers::models::siglip;
use crate::par::prelude::*;
const PROJ_WEIGHT: &str = "modality_projection.proj.weight";
enum Tower {
Ours(Box<crate::siglip::VisionTower>),
Candle(Box<siglip::VisionModel>),
}
pub struct SmolVlmVision {
tower: Tower,
proj: Tensor,
scale_factor: usize,
}
impl SmolVlmVision {
pub fn new(
cfg: &siglip::VisionConfig,
scale_factor: usize,
vision_prefix: &str,
connector_prefix: &str,
vb: VarBuilder,
) -> CandleResult<Self> {
let tower = if std::env::var_os("FFAI_ARGUS_CANDLE_TOWER").is_some() {
Tower::Candle(Box::new(siglip::VisionModel::new(
cfg,
false,
vb.pp(vision_prefix),
)?))
} else {
Tower::Ours(Box::new(crate::siglip::VisionTower::new(
cfg,
vb.pp(vision_prefix),
)?))
};
let proj = vb.pp(connector_prefix).get_unchecked(PROJ_WEIGHT)?;
let expect_in = cfg.hidden_size * scale_factor * scale_factor;
let (_out, got_in) = proj.dims2()?;
if got_in != expect_in {
candle_core::bail!(
"connector expects in_features {expect_in} \
(hidden {} x scale_factor {scale_factor}^2) but the checkpoint's \
{PROJ_WEIGHT} has {got_in} — the config and the weights disagree",
cfg.hidden_size
);
}
Ok(Self {
tower,
proj,
scale_factor,
})
}
pub fn tower(&self, pixel_values: &Tensor) -> CandleResult<Tensor> {
match &self.tower {
Tower::Ours(t) => t.forward(pixel_values),
Tower::Candle(t) => t.forward(pixel_values),
}
}
pub fn forward(&self, pixel_values: &Tensor) -> CandleResult<Tensor> {
let hidden = self.tower(pixel_values)?;
self.connect(&hidden)
}
pub fn connect(&self, hidden: &Tensor) -> CandleResult<Tensor> {
let (b, seq, dim) = hidden.dims3()?;
let s = self.scale_factor;
let side = (seq as f64).sqrt() as usize;
if side * side != seq {
candle_core::bail!("expected a square patch grid, got {seq} patches");
}
if !side.is_multiple_of(s) {
candle_core::bail!("patch grid {side} is not divisible by scale_factor {s}");
}
let x = if fused_shuffle() {
hidden.apply_op1_no_bwd(&PixelShuffleOp { side, s })?
} else {
let x = hidden.reshape((b, side, side, dim))?;
let x = x.reshape((b, side, side / s, dim * s))?;
let x = x.transpose(1, 2)?.contiguous()?;
let x = x.reshape((b, side / s, side / s, dim * s * s))?;
let x = x.transpose(1, 2)?.contiguous()?;
x.reshape((b, (side / s) * (side / s), dim * s * s))?
};
let (b, n, k) = x.dims3()?;
let w = self.proj.to_dtype(x.dtype())?.t()?;
let out = w.dim(1)?;
x.reshape((b * n, k))?.matmul(&w)?.reshape((b, n, out))
}
}
struct PixelShuffleOp {
side: usize,
s: usize,
}
impl candle_core::CustomOp1 for PixelShuffleOp {
fn name(&self) -> &'static str {
"ffai-pixel-shuffle"
}
fn cpu_fwd(
&self,
storage: &candle_core::CpuStorage,
layout: &candle_core::Layout,
) -> CandleResult<(candle_core::CpuStorage, candle_core::Shape)> {
let candle_core::CpuStorage::F32(x) = storage else {
candle_core::bail!("ffai-pixel-shuffle expects f32")
};
let Some((o, e)) = layout.contiguous_offsets() else {
candle_core::bail!("ffai-pixel-shuffle expects a contiguous input")
};
let x = &x[o..e];
let (b, seq, dim) = layout.shape().dims3()?;
let (side, s) = (self.side, self.s);
if side * side != seq || !side.is_multiple_of(s) || s == 0 {
candle_core::bail!("ffai-pixel-shuffle: {seq} patches, side {side}, s {s}");
}
let out_side = side / s;
let tokens = out_side * out_side;
let feat = dim * s * s;
let n = b * tokens * feat;
let mut out: Vec<f32> = Vec::with_capacity(n);
{
let spare = out.spare_capacity_mut();
#[allow(unsafe_code)]
let dst: &mut [f32] =
unsafe { std::slice::from_raw_parts_mut(spare.as_mut_ptr().cast::<f32>(), n) };
crate::cost::copy(n as u64);
let token = |(t, o): (usize, &mut [f32])| {
let (bb, rest) = (t / tokens, t % tokens);
let (rp, cp) = (rest / out_side, rest % out_side);
for i in 0..s {
for j in 0..s {
let row = (rp * s + i) * side + (cp * s + j);
let src = (bb * seq + row) * dim;
let at = (i * s + j) * dim;
o[at..at + dim].copy_from_slice(&x[src..src + dim]);
}
}
};
if crate::siglip::kernels_parallel_for_probe() {
dst.par_chunks_mut(feat).enumerate().for_each(token);
} else {
dst.chunks_mut(feat).enumerate().for_each(token);
}
}
#[allow(unsafe_code)]
unsafe {
out.set_len(n);
}
Ok((candle_core::CpuStorage::F32(out), (b, tokens, feat).into()))
}
}
fn fused_shuffle() -> bool {
FUSED_SHUFFLE.load(std::sync::atomic::Ordering::Relaxed)
}
pub fn set_fused_shuffle(on: bool) -> bool {
FUSED_SHUFFLE.swap(on, std::sync::atomic::Ordering::Relaxed)
}
static FUSED_SHUFFLE: std::sync::LazyLock<std::sync::atomic::AtomicBool> =
std::sync::LazyLock::new(|| {
std::sync::atomic::AtomicBool::new(
std::env::var("FFAI_ARGUS_FUSED_SHUFFLE").ok().as_deref() != Some("0"),
)
});
pub fn pixel_shuffle_for_probe(hidden: &Tensor, side: usize, s: usize) -> CandleResult<Tensor> {
if fused_shuffle() {
return hidden.apply_op1_no_bwd(&PixelShuffleOp { side, s });
}
let (b, _seq, dim) = hidden.dims3()?;
let x = hidden.reshape((b, side, side, dim))?;
let x = x.reshape((b, side, side / s, dim * s))?;
let x = x.transpose(1, 2)?.contiguous()?;
let x = x.reshape((b, side / s, side / s, dim * s * s))?;
let x = x.transpose(1, 2)?.contiguous()?;
x.reshape((b, (side / s) * (side / s), dim * s * s))
}
pub fn vision_config_from_json(config_json: &str) -> Result<(siglip::VisionConfig, usize), String> {
let v: serde_json::Value =
serde_json::from_str(config_json).map_err(|e| format!("config.json: {e}"))?;
let scale_factor = v
.get("scale_factor")
.and_then(serde_json::Value::as_u64)
.ok_or("config.json has no scale_factor")? as usize;
let vc = v.get("vision_config").ok_or("config.json has no vision_config")?;
let cfg: siglip::VisionConfig = serde_json::from_value(vc.clone())
.map_err(|e| format!("vision_config does not fit candle's SigLIP shape: {e}"))?;
Ok((cfg, scale_factor))
}
pub fn load(
weights: &std::path::Path,
config_json: &str,
device: &Device,
) -> Result<SmolVlmVision, String> {
let vb = unsafe {
VarBuilder::from_mmaped_safetensors(std::slice::from_ref(&weights), DType::F32, device)
}
.map_err(|e| format!("load {}: {e}", weights.display()))?;
load_vb(vb, config_json)
}
pub fn load_vb(vb: VarBuilder<'static>, config_json: &str) -> Result<SmolVlmVision, String> {
let (cfg, scale_factor) = vision_config_from_json(config_json)?;
SmolVlmVision::new(&cfg, scale_factor, "model.vision_model", "model.connector", vb)
.map_err(|e| format!("build vision tower: {e}"))
}
#[cfg(test)]
mod tests {
use super::{PixelShuffleOp, Tensor};
use candle_core::Device;
#[test]
fn fused_pixel_shuffle_matches_the_transpose_chain() {
let d = Device::Cpu;
for (side, s, dim) in [(8usize, 4usize, 6usize), (32, 4, 8), (6, 2, 3)] {
let seq = side * side;
let hidden = Tensor::arange(0f32, (seq * dim) as f32, &d)
.expect("arange")
.reshape((1, seq, dim))
.expect("reshape");
let fused =
hidden.apply_op1_no_bwd(&PixelShuffleOp { side, s }).expect("fused");
let x = hidden.reshape((1, side, side, dim)).expect("r1");
let x = x.reshape((1, side, side / s, dim * s)).expect("r2");
let x = x.transpose(1, 2).expect("t1").contiguous().expect("c1");
let x = x.reshape((1, side / s, side / s, dim * s * s)).expect("r3");
let x = x.transpose(1, 2).expect("t2").contiguous().expect("c2");
let want =
x.reshape((1, (side / s) * (side / s), dim * s * s)).expect("r4");
assert_eq!(fused.dims(), want.dims(), "shape at side {side} s {s}");
let got = fused.flatten_all().expect("f").to_vec1::<f32>().expect("v");
let want = want.flatten_all().expect("f").to_vec1::<f32>().expect("v");
assert_eq!(got, want, "pixel shuffle at side {side} s {s} dim {dim}");
}
}
}