//! Tiny-transformer hyperparameters. Host-only (no GPU), always compiled.

/// Architecture config for [`crate::TinyTransformer`]. Keep it tiny — T5 is a
/// correctness bring-up, not a real training run.
#[derive(Debug, Clone, Copy)]
pub struct Config {
    /// Vocabulary size (char-level in the bring-up).
    pub vocab: usize,
    /// Model / residual width. Must equal `n_heads * head_dim`.
    pub dim: usize,
    /// Number of decoder blocks.
    pub n_layers: usize,
    /// Number of attention heads.
    pub n_heads: usize,
    /// Per-head dimension (`dim / n_heads`).
    pub head_dim: usize,
    /// SwiGLU hidden width (gate/up project to this, down projects back).
    pub ffn_hidden: usize,
    /// RMSNorm epsilon.
    pub eps: f32,
    /// RoPE base frequency (theta).
    pub rope_theta: f32,
}

impl Config {
    /// A minimal config used by the bring-up / overfit test.
    pub fn tiny() -> Self {
        let n_heads = 2;
        let head_dim = 16;
        Config {
            vocab: 0, // set by the caller from the char vocab
            dim: n_heads * head_dim,
            n_layers: 2,
            n_heads,
            head_dim,
            ffn_hidden: 64,
            eps: 1e-5,
            rope_theta: 10000.0,
        }
    }

    /// Build a config from the architecture knobs, deriving `dim = n_heads *
    /// head_dim`. The scaling-run entry (`bin/train`) passes these from CLI so the
    /// model size is a tunable ladder rung (v1 = dim256/8L, v2/v3 scale further),
    /// instead of a hardcoded tiny config. `eps`/`rope_theta` keep the engine
    /// defaults (also what the xserv export reconciles against).
    pub fn from_arch(
        vocab: usize,
        n_heads: usize,
        head_dim: usize,
        n_layers: usize,
        ffn_hidden: usize,
    ) -> Self {
        Config {
            vocab,
            dim: n_heads * head_dim,
            n_layers,
            n_heads,
            head_dim,
            ffn_hidden,
            eps: 1e-5,
            rope_theta: 10000.0,
        }
    }

    /// Transformer-core parameter count: everything except the token embedding and
    /// the LM head (the two `vocab × dim` tables). This is the figure the scaling
    /// ladder is sized against — the 50257-vocab embed+lm_head adds a fixed ~25M on
    /// top that does not reflect model capacity. `num_params() = core + 2·vocab·dim`.
    pub fn core_params(&self) -> usize {
        self.num_params() - 2 * self.vocab * self.dim
    }

    /// Total learnable parameter count (for logging / sanity).
    pub fn num_params(&self) -> usize {
        let per_layer = 2 * self.dim                       // 2 rmsnorm gammas
            + 2 * self.head_dim                             // q/k per-head norm gammas
            + 3 * self.dim * self.dim                       // q/k/v proj
            + self.dim * self.dim                           // out proj
            + 2 * self.dim * self.ffn_hidden                // gate/up proj
            + self.ffn_hidden * self.dim; // down proj
        self.vocab * self.dim          // embedding
            + self.n_layers * per_layer
            + self.dim                 // final norm
            + self.dim * self.vocab // lm head
    }
}
