libmir-cuda 0.2.0

CUDA inference backend for libmir
use models::layout::EncoderRopeScaling;

use super::{
    CudaSequenceScoringModel,
    execute::{epsilon, f16_tensor, ops, project},
    scratch::Scratch,
};
use crate::{
    Error, Result,
    kernels::{EncoderAttentionF16, EncoderAttentionSpec},
};

impl CudaSequenceScoringModel {
    #[expect(clippy::too_many_lines, reason = "keeps one encoder layer contract contiguous")]
    pub(super) fn layer(&self, layer: usize, scratch: &mut Scratch) -> Result<()> {
        let config = &self.config;
        let count = scratch.tokens;
        let hidden = config.hidden_size;
        let prefix = format!("new.encoder.layer.{layer}");
        let (input, output) = if layer.is_multiple_of(2) {
            (&scratch.first, &mut scratch.second)
        } else {
            (&scratch.second, &mut scratch.first)
        };
        project(
            &self.backend,
            count,
            hidden,
            hidden * 3,
            input,
            self.tensor(&format!("{prefix}.attention.qkv_proj.weight"))?,
            &mut scratch.qkv_raw,
        )?;
        ops(&self.backend, count, hidden * 3, epsilon(config)?)?.add_bias(
            &self.backend.inner.stream,
            &scratch.qkv_raw,
            f16_tensor(self.tensor(&format!("{prefix}.attention.qkv_proj.bias"))?)?,
            &mut scratch.qkv,
        )?;
        EncoderAttentionF16::compile(
            &self.backend.inner.compiler,
            EncoderAttentionSpec {
                tokens: count,
                heads: config.num_attention_heads,
                head_dim: config.head_dim,
                theta: config.rope_theta.unwrap_or(10_000.0).to_string().parse()?,
                ntk_factor: ntk_factor(config)?,
            },
        )?
        .execute(&self.backend.inner.stream, &scratch.qkv, &mut scratch.attention)?;
        project(
            &self.backend,
            count,
            hidden,
            hidden,
            &scratch.attention,
            self.tensor(&format!("{prefix}.attention.o_proj.weight"))?,
            &mut scratch.projection_raw,
        )?;
        ops(&self.backend, count, hidden, epsilon(config)?)?.add_bias(
            &self.backend.inner.stream,
            &scratch.projection_raw,
            f16_tensor(self.tensor(&format!("{prefix}.attention.o_proj.bias"))?)?,
            &mut scratch.projection,
        )?;
        ops(&self.backend, count, hidden, epsilon(config)?)?.residual_norm(
            &self.backend.inner.stream,
            input,
            &scratch.projection,
            f16_tensor(self.tensor(&format!("{prefix}.attn_ln.weight"))?)?,
            f16_tensor(self.tensor(&format!("{prefix}.attn_ln.bias"))?)?,
            output,
        )?;
        project(
            &self.backend,
            count,
            hidden,
            config.intermediate_size * 2,
            output,
            self.tensor(&format!("{prefix}.mlp.up_gate_proj.weight"))?,
            &mut scratch.up_gate,
        )?;
        ops(&self.backend, count, config.intermediate_size, epsilon(config)?)?.gated_gelu(
            &self.backend.inner.stream,
            &scratch.up_gate,
            &mut scratch.activated,
        )?;
        project(
            &self.backend,
            count,
            config.intermediate_size,
            hidden,
            &scratch.activated,
            self.tensor(&format!("{prefix}.mlp.down_proj.weight"))?,
            &mut scratch.down_raw,
        )?;
        ops(&self.backend, count, hidden, epsilon(config)?)?.add_bias(
            &self.backend.inner.stream,
            &scratch.down_raw,
            f16_tensor(self.tensor(&format!("{prefix}.mlp.down_proj.bias"))?)?,
            &mut scratch.down,
        )?;
        ops(&self.backend, count, hidden, epsilon(config)?)?.residual_norm(
            &self.backend.inner.stream,
            output,
            &scratch.down,
            f16_tensor(self.tensor(&format!("{prefix}.mlp_ln.weight"))?)?,
            f16_tensor(self.tensor(&format!("{prefix}.mlp_ln.bias"))?)?,
            &mut scratch.projection_raw,
        )?;
        self.backend.inner.stream.copy_device_range(
            &scratch.projection_raw,
            0..count * hidden,
            output,
            0,
        )?;
        Ok(())
    }
}

fn ntk_factor(config: &models::layout::EncoderConfig) -> Result<f32> {
    let Some(EncoderRopeScaling::Ntk { factor, mixed_b: None }) = config.rope_scaling else {
        return Err(Error::InvalidDecoderKernel("encoder fixed NTK factor is missing"));
    };
    Ok(factor.to_string().parse()?)
}