metrale_model_layers/layers/vision_tower.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The vision tower a built model holds, whichever family bound it.
4//!
5//! Owner: model-layers (vision).
6//! Invariants: none beyond the types.
7//!
8//! Two towers exist: the Qwen3-VL-shaped [`VisionEncoder`] and GLM-5.3's
9//! [`GlmVit`]. The engine uses them only through this enum:
10//!
11//! * `forward_batched(images) -> per-image (post_h, post_w, merged_p)`,
12//! * a packed `[Ī£merged_p, out_hidden_size]` BF16 output, image-ordered, read
13//! through `out_row`,
14//! * `out_hidden_size`,
15//! * `ensure_scratch`, for a tensor-parallel worker that receives rows instead
16//! of encoding.
17//!
18//! An enum rather than a trait object, so adding a tower means adding a
19//! variant to every match here.
20
21use anyhow::Result;
22use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
23
24use super::glm_vit::GlmVit;
25use super::vision_encoder::VisionEncoder;
26
27pub enum VisionTower {
28 /// 2026-09-25: Built by `weight_loader::qwen3_vl` and `weight_loader::qwen35`
29 /// (which the qwen35_dense and qwen4_exp loaders call).
30 Qwen(Box<VisionEncoder>),
31 /// 2026-09-25: GLM-5.3-Flash (`glm5_next`).
32 Glm(Box<GlmVit>),
33}
34
35impl VisionTower {
36 pub fn qwen(encoder: VisionEncoder) -> Self {
37 Self::Qwen(Box::new(encoder))
38 }
39
40 pub fn glm(encoder: GlmVit) -> Self {
41 Self::Glm(Box::new(encoder))
42 }
43
44 /// 2026-09-25: Encode a batch of `(pixels, grid_h, grid_w)` temporal groups. Returns
45 /// `(post_merge_h, post_merge_w, merged_patches)` per group, in order.
46 pub fn forward_batched(
47 &self,
48 images: &[(&[f32], usize, usize)],
49 gpu: &dyn GpuBackend,
50 stream: u64,
51 ) -> Result<Vec<(usize, usize, usize)>> {
52 match self {
53 Self::Qwen(e) => e.forward_batched(images, gpu, stream),
54 Self::Glm(e) => e.forward_batched(images, gpu, stream),
55 }
56 }
57
58 /// 2026-09-25: Width of one merged embedding row.
59 pub fn out_hidden_size(&self) -> usize {
60 match self {
61 Self::Qwen(e) => e.out_hidden_size,
62 Self::Glm(e) => e.out_hidden_size,
63 }
64 }
65
66 /// 2026-09-25: Allocate the encoder's device scratch if it is not allocated yet.
67 ///
68 /// Normally the first image does this. A tensor-parallel worker receives
69 /// the merged rows by broadcast instead of encoding, so it calls this
70 /// before `out_row` names a destination.
71 pub fn ensure_scratch(&self, gpu: &dyn GpuBackend) -> Result<()> {
72 match self {
73 Self::Qwen(e) => e.scratch_init(gpu),
74 Self::Glm(e) => e.scratch_init(gpu),
75 }
76 }
77
78 /// 2026-09-25: Device pointer to merged row `row` of the packed output. The
79 /// Qwen arm panics unless `forward_batched` or `ensure_scratch` has
80 /// allocated the scratch.
81 pub fn out_row(&self, row: usize) -> DevicePtr {
82 match self {
83 Self::Qwen(e) => e.scratch().buf_out.offset(row * e.out_hidden_size * 2),
84 Self::Glm(e) => e.out_row(row),
85 }
86 }
87}