Skip to content

Phase 1: a working f32 forward pass (generates coherent text) - #2

Merged
codewithfourtix merged 10 commits into
mainfrom
feat/phase-1-forward-pass
Jul 14, 2026
Merged

Phase 1: a working f32 forward pass (generates coherent text)#2
codewithfourtix merged 10 commits into
mainfrom
feat/phase-1-forward-pass

Conversation

@codewithfourtix

Copy link
Copy Markdown
Owner

Implements the whole Phase 1 pipeline — a hand-written transformer that loads a real Qwen2.5 model and generates text.

What landed

  • Kernelsmatvec (rayon), rms_norm, rope (HF rotate-half), grouped-query attention + causal KV cache, swiglu, softmax
  • Model — safetensors loading (bf16/f16/f32 to f32), Qwen tied LM head + QKV biases, full 24-layer forward pass
  • CLI — tokenizer + prefill + streaming greedy/top-p generation loop
  • Oracle — a standalone NumPy mirror of the exact algorithm

Verified

The NumPy oracle (identical math to the Rust) run against Qwen2.5-0.5B:

The capital of France is Paris. It is the largest city in Europe and the third

Coherent, on-topic output confirms every convention is correct — RoPE pairing, GQA head mapping, tied embeddings, biases.

Next: Phase 2 (quantization + benchmarks).

Row-major matrix-vector product with each output row computed in parallel, plus
the in-place residual/bias adds.
RoPE uses the HuggingFace rotate-half convention (dim j pairs with j+head_dim/2)
that Llama/Qwen weights expect.
Each query head attends over the causal KV cache using its shared kv head
(group = n_heads / n_kv), scaled by 1/sqrt(head_dim).
Greedy arg-max plus top-p with a small dependency-free xorshift RNG.
default-features = false selects the pure-Rust regex path, so no C/C++ toolchain
is needed to compile the tokenizer.
Loads every tensor (bf16/f16/f32 -> f32), handles Qwen's tied LM head and QKV
biases, and runs embed -> N x (RMSNorm -> GQA -> RMSNorm -> SwiGLU) -> norm ->
LM head for one decode step.
Encodes the prompt, prefills the KV cache, then samples/decodes/streams tokens
until EOS or the token budget, reporting tokens/sec.
A standalone NumPy mirror of the exact algorithm. Verified against Qwen2.5-0.5B:
'The capital of France is Paris. It is the largest city in Europe and the third'.
Copilot AI review requested due to automatic review settings July 14, 2026 07:42
@codewithfourtix
codewithfourtix merged commit 95d9d49 into main Jul 14, 2026
@codewithfourtix
codewithfourtix deleted the feat/phase-1-forward-pass branch July 14, 2026 07:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the Phase 1 (correctness-first) pipeline for ember: it loads Qwen2.5 weights from safetensors, runs a full f32 transformer forward pass with a causal GQA KV-cache, and generates text via greedy or top‑p sampling. It also adds a NumPy “oracle” implementation to cross-check the Rust implementation token-for-token.

Changes:

  • Implemented core kernels and ops (rayon matvec, RMSNorm, RoPE rotate-half, SwiGLU, softmax) and wired them into attention + the full multi-layer forward pass.
  • Added safetensors mmap loading (bf16/f16/f32 → f32), KV-cache sizing cap, and tied-LM-head handling.
  • Implemented CLI generation loop with tokenizer prefill + streaming decode, plus a standalone NumPy reference forward pass and updated project docs.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/tensor.rs Implements rayon-parallel row-major matvec and adds add_bias helper.
src/ops.rs Implements RMSNorm, HF rotate-half RoPE, SwiGLU, and numerically-stable softmax.
src/attention.rs Implements grouped-query attention over a causal KV cache and wires in softmax.
src/model.rs Adds safetensors weight loading + full per-token forward pass across all layers.
src/sample.rs Implements greedy + top‑p sampling and adds a small dependency-free RNG.
src/main.rs Wires CLI + tokenizer to prefill and decode loop with streaming output + timing.
scripts/reference_forward.py Adds a NumPy oracle mirroring Rust forward pass for correctness checks.
README.md Updates status/docs to “Phase 1 complete” and adds an example run + references.
PHASES.md Introduces a phased build plan and documents Phase 1 completion criteria.
Cargo.toml Disables default tokenizers features to avoid requiring a C toolchain.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/ops.rs
Comment on lines 26 to +28
pub fn rope(vec: &mut [f32], pos: usize, head_dim: usize, theta: f32) {
debug_assert_eq!(vec.len() % head_dim, 0);
// For each (even, odd) dimension pair `i`, rotate by angle
// pos / theta^(2i / head_dim).
let _ = (vec, pos, head_dim, theta);
todo!("RoPE rotation of (even, odd) dimension pairs")
debug_assert_eq!(vec.len(), head_dim);
let half = head_dim / 2;
Comment thread src/attention.rs
Comment on lines +96 to +100
let hd = config.head_dim();
let n_heads = config.num_attention_heads;
let n_kv = config.num_key_value_heads;
let group = n_heads / n_kv;
let kv_dim = config.kv_dim();
Comment thread src/sample.rs
Comment on lines 19 to +22
Sampler::TopP { temperature, top_p } => {
// Divide logits by `temperature`, softmax, keep the smallest set
// of tokens whose probability mass ≥ `top_p`, renormalise, draw.
let _ = (temperature, top_p);
todo!("temperature + nucleus (top-p) sampling")
let mut probs: Vec<f32> = logits.iter().map(|&l| l / temperature).collect();
softmax(&mut probs);

Comment thread src/model.rs
Comment on lines +171 to +187
let bytes = t.data();
let out = match t.dtype() {
Dtype::F32 => bytes
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect(),
Dtype::F16 => bytes
.chunks_exact(2)
.map(|c| half::f16::from_bits(u16::from_le_bytes([c[0], c[1]])).to_f32())
.collect(),
Dtype::BF16 => bytes
.chunks_exact(2)
.map(|c| half::bf16::from_bits(u16::from_le_bytes([c[0], c[1]])).to_f32())
.collect(),
other => bail!("tensor {name}: unsupported dtype {other:?}"),
};
Ok(out)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants