libmir-cuda 0.3.0

CUDA inference backend for libmir
use models::weights::{
    BlockProjectionLayout, BlockQuantization, RoutedDecoderLayerBindings, RoutedExpertBindings,
    TensorBinding, TensorStorage,
};

use super::{ClampedRoutedConfig, ClampedRoutedExpertWeights, NativeExpertWeights, tensor};
use crate::{
    CudaBackend, CudaTensorSet, Result,
    backend::clamped_routed::{
        projection::{ClampedRoutedLinearWeight, ClampedRoutedQkvProjections},
        validation::validate_native_experts,
    },
};

pub(super) fn load(
    backend: &CudaBackend,
    config: ClampedRoutedConfig,
    tensors: &CudaTensorSet,
    bindings: RoutedDecoderLayerBindings<'_>,
) -> Result<(
    ClampedRoutedQkvProjections,
    ClampedRoutedLinearWeight,
    ClampedRoutedLinearWeight,
    ClampedRoutedExpertWeights,
)> {
    let q = tensor(tensors, &bindings.query.source)?;
    let k = tensor(tensors, &bindings.key.source)?;
    let v = tensor(tensors, &bindings.value.source)?;
    let RoutedExpertBindings::InterleavedGateUp { gate_up, down } = bindings.experts else {
        return Err(crate::Error::InvalidDecoderKernel(
            "native clamped-routed requires interleaved gate/up expert bindings",
        ));
    };
    let (gate_up_scales, gate_up_bias) = block_companions(
        gate_up,
        BlockProjectionLayout::FusedGateUpBank {
            experts: config.experts,
            interleaved: true,
        },
    )?;
    let (down_scales, down_bias) =
        block_companions(down, BlockProjectionLayout::MatrixBank { matrices: config.experts })?;
    let experts = ClampedRoutedExpertWeights::Native(Box::new(NativeExpertWeights {
        gate_up_blocks: tensor(tensors, &gate_up.source)?,
        gate_up_scales: tensor(tensors, gate_up_scales)?,
        gate_up_bias: tensor(tensors, gate_up_bias)?,
        down_blocks: tensor(tensors, &down.source)?,
        down_scales: tensor(tensors, down_scales)?,
        down_bias: tensor(tensors, down_bias)?,
    }));
    validate_native_experts(config, &experts)?;
    Ok((
        ClampedRoutedQkvProjections::Native(backend.pack_bf16_linears([&q, &k, &v])?),
        ClampedRoutedLinearWeight::Native(tensor(tensors, &bindings.attention_output.source)?),
        ClampedRoutedLinearWeight::Native(tensor(tensors, &bindings.router.source)?),
        experts,
    ))
}

fn block_companions(
    binding: &TensorBinding,
    expected: BlockProjectionLayout,
) -> Result<(&str, &str)> {
    let TensorStorage::BlockQuantized {
        format: BlockQuantization::MXFP4,
        scales,
        bias: Some(bias),
        ..
    } = &binding.storage
    else {
        return Err(crate::Error::InvalidDecoderKernel(
            "native clamped-routed expert binding requires MXFP4 scales and bias",
        ));
    };
    if binding.block_projection_layout() != Some(expected) {
        return Err(crate::Error::InvalidDecoderKernel(
            "native clamped-routed expert binding has the wrong matrix-bank layout",
        ));
    }
    Ok((scales, bias))
}

#[cfg(test)]
mod tests {
    use models::weights::{
        BindingTransform, ExpertProjectionRole, LayerTensorRole, LogicalTensorRole, TensorPacking,
    };

    use super::*;

    #[test]
    fn requires_typed_interleaved_expert_bank() -> Result<()> {
        let valid = binding(TensorPacking::InterleavedGateUp);
        let expected = BlockProjectionLayout::FusedGateUpBank { experts: 8, interleaved: true };
        assert_eq!(block_companions(&valid, expected)?, ("scales", "bias"));

        let invalid = binding(TensorPacking::Separate);
        assert!(block_companions(&invalid, expected).is_err());
        Ok(())
    }

    fn binding(packing: TensorPacking) -> TensorBinding {
        TensorBinding {
            role: LogicalTensorRole::Layer {
                index: 0,
                tensor: LayerTensorRole::ExpertProjection {
                    expert: None,
                    projection: ExpertProjectionRole::GateUp,
                },
            },
            source: "blocks".into(),
            shape: vec![8, 64, 1, 16],
            logical_shape: Some(vec![8, 64, 32]),
            transforms: vec![
                BindingTransform::StackedExperts { count: 8 },
                BindingTransform::FusedGateUp { interleaved: true },
            ],
            storage: TensorStorage::BlockQuantized {
                format: BlockQuantization::MXFP4,
                scales: "scales".into(),
                global_scale: None,
                input_scale: None,
                bias: Some("bias".into()),
                packing,
            },
        }
    }
}