use crate::builder::WhisperGraphOpts;
use crate::config::WhisperConfig;
use crate::flow::{
WhisperEncoderFlow, build_whisper_align_hidden_built_ext_opts, build_whisper_cross_kv_built,
build_whisper_decode_step_built_ext_opts, build_whisper_decoder_prefill_built_ext_opts,
};
use crate::fused::{FusedDecoderWeights, FusedEncoderWeights};
use crate::weights::WhisperWeightPrefix;
use anyhow::Result;
use rlx_core::device_supports_gpu_kv;
use rlx_core::flow_bridge::{compile_options_for_profile, profile_near_weights};
use rlx_core::weight_map::WeightMap;
use rlx_flow::BuiltModel;
use rlx_flow::CompileProfile;
use rlx_opt::PrecisionPolicy;
use rlx_runtime::compile_cache::BucketedCompileCache;
use rlx_runtime::{CompileOptions, Device, Precision};
use std::collections::HashMap;
use std::ops::Range;
use std::path::Path;
use std::sync::Arc;
type WeightTensor = (Vec<f32>, Vec<usize>);
type WhisperWeightMap = HashMap<String, WeightTensor>;
pub fn decode_cache_key(batch: usize, seq: usize) -> u64 {
((batch as u64) << 32) | (seq as u64)
}
pub fn geometry_cache_key(geometry_epoch: u64, batch: usize) -> u64 {
(geometry_epoch << 32) | batch as u64
}
pub fn prefill_cache_key(geometry_epoch: u64, batch: usize, dec_seq: usize) -> u64 {
geometry_epoch ^ ((batch as u64) << 24) ^ (dec_seq as u64)
}
pub fn align_cache_key(geometry_epoch: u64, dec_seq: usize, max_layer: usize) -> u64 {
geometry_epoch ^ ((dec_seq as u64) << 20) ^ (max_layer as u64)
}
pub fn decode_bucket_ladder(device: Device, max_past: u64) -> BucketedCompileCache {
let max_past = max_past.max(1);
#[allow(clippy::single_range_in_vec_init)]
let mut ranges: Vec<Range<u64>> = vec![0..2];
let mut start = 2u64;
let mut extent = 4u64;
loop {
ranges.push(start..(extent + 1));
if extent >= max_past {
break;
}
start = extent + 1;
extent = extent.saturating_mul(2).max(start + 1);
}
BucketedCompileCache::with_policy(device, ranges, None)
}
pub(crate) fn metal_safe_decode_profile(
device: Device,
mut profile: CompileProfile,
) -> CompileProfile {
if device == Device::Metal {
profile.fusion.skip = true;
profile.backend.metal.skip_fusion = true;
profile.backend.metal.unfuse_regions = true;
}
profile
}
fn apply_f16_compute(opts: &mut CompileOptions, f16: bool) {
if f16 {
opts.precision = Precision::F16;
opts.policy = Some(PrecisionPolicy::AutoMixed);
}
}
#[derive(Debug, Clone)]
pub struct WhisperCompileOpts {
pub encoder: CompileOptions,
pub cross: CompileOptions,
pub prefill: CompileOptions,
pub decode: CompileOptions,
pub align: CompileOptions,
}
pub fn whisper_use_gpu_kv(lm_device: Device, decode_device: Device) -> bool {
device_supports_gpu_kv(decode_device) && decode_device == lm_device
}
pub fn whisper_decoder_device(lm_device: Device) -> Device {
match lm_device {
Device::Metal | Device::Mlx | Device::Vulkan => Device::Cpu,
other => other,
}
}
pub fn whisper_encoder_device(lm_device: Device) -> Device {
match lm_device {
Device::Mlx => Device::Cpu,
other => other,
}
}
pub fn metal_compile_guard<R, F>(device: Device, f: F) -> R
where
F: FnOnce() -> R,
{
if device == Device::Metal {
rlx_ir::env::set("RLX_DISABLE_MPSGRAPH", "1");
let out = f();
rlx_ir::env::unset("RLX_DISABLE_MPSGRAPH");
out
} else {
f()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rlx_runtime::Device;
#[test]
fn decode_bucket_ladder_includes_past_zero() {
let cache = decode_bucket_ladder(Device::Cpu, 448);
assert!(cache.bucket_for(0).is_some());
assert!(cache.bucket_for(1).is_some());
assert!(cache.bucket_for(200).is_some());
}
#[test]
fn decode_bucket_ladder_is_logarithmic() {
let cache = decode_bucket_ladder(Device::Cpu, 448);
assert!(
cache.total_buckets() <= 12,
"expected O(log n) buckets, got {}",
cache.total_buckets()
);
assert_eq!(cache.bucket_for(0), cache.bucket_for(1));
assert_eq!(cache.bucket_for(5), cache.bucket_for(8));
}
#[test]
fn stage_device_routing() {
assert_eq!(whisper_encoder_device(Device::Cpu), Device::Cpu);
assert_eq!(whisper_decoder_device(Device::Cpu), Device::Cpu);
assert_eq!(whisper_encoder_device(Device::Metal), Device::Metal);
assert_eq!(whisper_decoder_device(Device::Metal), Device::Cpu);
assert_eq!(whisper_encoder_device(Device::Mlx), Device::Cpu);
assert_eq!(whisper_decoder_device(Device::Mlx), Device::Cpu);
assert_eq!(whisper_encoder_device(Device::Gpu), Device::Gpu);
assert_eq!(whisper_decoder_device(Device::Gpu), Device::Gpu);
assert_eq!(whisper_decoder_device(Device::Vulkan), Device::Cpu);
}
}
impl WhisperCompileOpts {
pub fn new(device: Device, f16_compute: bool, weights: &Path) -> Self {
let encoder_profile =
profile_near_weights(weights, "whisper.rlx.toml", CompileProfile::encoder());
let decode_profile = metal_safe_decode_profile(device, CompileProfile::gemma_decode());
let prefill_profile = metal_safe_decode_profile(device, CompileProfile::llama32_prefill());
let mut encoder = compile_options_for_profile(&encoder_profile, device);
let mut cross = compile_options_for_profile(&CompileProfile::encoder(), device);
let mut prefill = compile_options_for_profile(&prefill_profile, device);
let mut decode = compile_options_for_profile(&decode_profile, device);
apply_f16_compute(&mut encoder, f16_compute);
apply_f16_compute(&mut cross, f16_compute);
apply_f16_compute(&mut prefill, f16_compute);
apply_f16_compute(&mut decode, f16_compute);
let align = encoder.clone();
Self {
encoder,
cross,
prefill,
decode,
align,
}
}
}
#[derive(Clone)]
pub struct WhisperGraphCtx {
pub cfg: WhisperConfig,
pub pfx: WhisperWeightPrefix,
pub weights: Arc<WhisperWeightMap>,
pub enc_seq: usize,
pub mel_frames: usize,
pub graph_opts: WhisperGraphOpts,
pub fused: Option<FusedDecoderWeights>,
pub fused_enc: Option<FusedEncoderWeights>,
}
impl WhisperGraphCtx {
pub fn weight_map(&self) -> WeightMap {
WeightMap::from_tensors((*self.weights).clone())
}
fn weight_map_excluding(&self, exclude: impl Fn(&str) -> bool) -> WeightMap {
let mut m: WhisperWeightMap = HashMap::with_capacity(self.weights.len());
for (k, v) in self.weights.iter() {
if !exclude(k) {
m.insert(k.clone(), v.clone());
}
}
WeightMap::from_tensors(m)
}
fn weight_map_encoder(&self) -> WeightMap {
let dec = self.pfx.decoder.clone();
self.weight_map_excluding(move |k| k.starts_with(&dec) || k.starts_with("fused.dec"))
}
fn weight_map_decoder(&self) -> WeightMap {
let enc = self.pfx.encoder.clone();
self.weight_map_excluding(move |k| k.starts_with(&enc) || k.starts_with("fused.enc"))
}
pub fn build_encoder(&self, batch: usize) -> Result<BuiltModel> {
self.build_encoder_sized(batch, self.mel_frames)
}
pub fn build_encoder_sized(&self, batch: usize, mel_frames: usize) -> Result<BuiltModel> {
let mut wm = self.weight_map_encoder();
WhisperEncoderFlow::new_opts(
&self.cfg,
&wm,
batch,
mel_frames,
self.graph_opts,
self.fused_enc.as_ref(),
)
.build(&mut wm)
}
pub fn build_cross(&self, batch: usize) -> Result<BuiltModel> {
self.build_cross_sized(batch, self.enc_seq)
}
pub fn build_cross_sized(&self, batch: usize, enc_seq: usize) -> Result<BuiltModel> {
let mut wm = self.weight_map_decoder();
build_whisper_cross_kv_built(&self.cfg, &mut wm, &self.pfx, batch, enc_seq)
}
pub fn build_prefill(&self, batch: usize, dec_seq: usize) -> Result<BuiltModel> {
self.build_prefill_sized(batch, dec_seq, self.enc_seq)
}
pub fn build_prefill_sized(
&self,
batch: usize,
dec_seq: usize,
enc_seq: usize,
) -> Result<BuiltModel> {
let mut wm = self.weight_map_decoder();
build_whisper_decoder_prefill_built_ext_opts(
&self.cfg,
&mut wm,
&self.pfx,
batch,
dec_seq,
enc_seq,
true,
self.graph_opts,
self.fused.as_ref(),
)
}
pub fn build_decode_step(&self, batch: usize, bucket_upper: usize) -> Result<BuiltModel> {
self.build_decode_step_sized(batch, bucket_upper, self.enc_seq)
}
pub fn build_decode_step_sized(
&self,
batch: usize,
bucket_upper: usize,
enc_seq: usize,
) -> Result<BuiltModel> {
let mut wm = self.weight_map_decoder();
build_whisper_decode_step_built_ext_opts(
&self.cfg,
&mut wm,
&self.pfx,
batch,
bucket_upper,
enc_seq,
true,
self.graph_opts,
self.fused.as_ref(),
)
}
pub fn build_align_hidden_sized(
&self,
batch: usize,
dec_seq: usize,
enc_seq: usize,
max_layer: usize,
) -> Result<BuiltModel> {
let mut wm = self.weight_map_decoder();
build_whisper_align_hidden_built_ext_opts(
&self.cfg,
&mut wm,
&self.pfx,
batch,
dec_seq,
enc_seq,
max_layer,
self.graph_opts,
self.fused.as_ref(),
)
}
}