Sistem Programlama ve Yapay Zeka: Rust ile Özel GPT-2 Decoder Geliştirmek
Rust ve tch-rs ile GPT tarzı decoder blokları ve eğitim döngüleri kurmaya yönelik sistem odaklı bir rehber.
Python çalışma zamanı yükünden uzaklaşarak tip güvenli, derlenmiş hızda makine öğrenmesi modelleri ve eğitim döngüleri geliştirmeye dair teknik bir rehber.
Python uzun yıllardır yapay zeka geliştirme için varsayılan dil oldu. PyTorch, TensorFlow, JAX, Jupyter ve Google Colab gibi araçlar hızlı araştırma ve prototipleme için çok güçlüdür.
Ancak yapay zeka iş yükleri araştırmadan üretim ortamlarına geçtikçe Python'ın bazı sınırları görünür hale gelir:
- Global Interpreter Lock (GIL): Gerçek çok iş parçacıklı paralel yürütmeyi sınırlar.
- Yüksek bellek yükü: Dinamik tip sistemi ve garbage collection ek bellek tüketir.
- Derleme zamanı kontrol eksikliği: Tensor şekli veya tip hataları çoğu zaman ancak çalışma sırasında ortaya çıkar.
Sistem seviyesinde kontrol isteyen geliştiriciler için Rust güçlü bir alternatiftir. tch-rs, PyTorch'un C++ motoru olan libtorch üzerine Rust bağlayıcıları sağlar. Böylece PyTorch performansını Rust'ın bellek güvenliği ve tip sistemiyle birlikte kullanmak mümkün olur.
Rust AI Stack: Neden tch-rs?
Python'da import torch yazdığınızda aslında yüksek performanslı C++ motoru olan libtorch için bir sarmalayıcı kullanırsınız.
tch-rs, bu C++ kütüphanesine doğrudan Rust bağlayıcıları sağlar. Bu, sıfırdan CUDA kernel yazmadan veya derin öğrenme matematiğini baştan kurmadan PyTorch'un tensor performansını, GPU hızlandırmasını ve backpropagation altyapısını kullanmak anlamına gelir.
┌──────────────────────────────────────────────────┐
│ Rust Application Code │
└───────────┬──────────────────────────────────────┘
│ FFI (Foreign Function Interface)
▼
tch-rs Rust Bindings
│
▼
PyTorch C++ (libtorch)
│
┌───────┴───────┐
▼ ▼
CUDA Kernels MPS Kernels
1. Causal Self-Attention Matematiği
GPT-2 gibi üretici transformer modellerinde attention katmanı query (), key () ve value () matrislerini kullanarak token bağımlılıklarını hesaplar. Causal decoder yapısında modelin gelecekteki tokenlara bakması engellenmelidir. Bunun için gelecekteki pozisyonları yapan alt üçgensel maske matrisi () kullanılır:
Burada:
- , , giriş embeddinglerinden üretilen Query, Key ve Value matrisleridir:
- , key boyutudur ve softmax sırasında gradyanların kaybolmasını azaltmak için ölçekleme sağlar.
- causal mask matrisidir:
Rust tarafında bu maske tch-rs ile oluşturulabilir:
// src/model.rs
impl Module for CausalSelfAttention {
fn forward(&self, x: &Tensor) -> Tensor {
let kind = x.kind();
let (_b, t, _c) = x.size3().unwrap();
let k = self.key_linear.forward(&x);
let q = self.query_linear.forward(&x);
let v = self.value_linear.forward(&x);
let mask = Tensor::ones([t, t], (kind, self.device))
.tril(0)
.reshape([1, t, t]);
Tensor::scaled_dot_product_attention(&q, &k, &v, Option::Some(mask), 0.1, false, Option::None)
}
}
2. Rust'ta Bellek Sürekliliği
Python/PyTorch'ta view gibi işlemler, tensor belleği uygun değilse çoğu zaman çalışma sırasında hata üretir. Rust tch-rs tarafında ise transpoze edilmiş bir tensörü .contiguous() çağırmadan yeniden şekillendirmek alttaki libtorch motorunda paniğe yol açabilir.
Çünkü transpoze işlemleri veriyi fiziksel olarak yeniden dizmek yerine layout metadata'sını değiştirir. Feed-forward ve attention çıktıları birleştirilirken bellek sürekliliği açıkça sağlanmalıdır:
// Rust içinde bellek düzenleme bloğu
let transposed = attention_output.transpose(1, 2);
let contiguous_tensor = transposed.contiguous();
let reshaped = contiguous_tensor.view([batch_size, seq_len, embed_dim]);
3. Derlenmiş Eğitim Döngüsü Yazmak
Python'da model parametreleri çoğu zaman arka planda daha örtük şekilde güncellenir. Rust'ta optimizer ve backpropagation adımları açık biçimde kurulur:
// src/main.rs
fn main() -> anyhow::Result<()> {
let block_size = 128;
let vocab_size = 968;
let device = find_device();
let vs = nn::VarStore::new(device);
let mut opt = nn::AdamW::default().build(&vs, 1e-4)?;
let model = Gpt::new(vs.root(), vocab_size, 128, block_size, 4, 4);
for epoch in 0..100 {
let (xs, ys, _) = dataset::get_batch_train();
let logits = model.forward(&xs.to_kind(Kind::Int64).to_device(device));
let (b, t, c) = logits.size3().unwrap();
let logits = logits.view([b * t, c]);
let targets = ys.to_kind(Kind::Int64).to_device(device).view([b * t]);
let loss = logits.cross_entropy_for_logits(&targets);
opt.backward_step(&loss);
if epoch % 10 == 0 {
println!("Epoch: {}, Loss: {:?}", epoch, loss);
}
}
Ok(())
}
PyTorch ve tch-rs Arasındaki Temel Farklar
- Atomik gradyan güncellemeleri: Python'da
optimizer.zero_grad(),loss.backward()veoptimizer.step()ayrı ayrı çağrılır. Rust tarafındaopt.backward_step(&loss)boilerplate kodu azaltır. - Açık reshape işlemleri: Python dinamik slice sözdizimi sunar. Rust tarafında
.view()ve.reshape()gibi dönüşümler daha açık yazılır. - Variable Store (
VarStore): Rust değişkenleri isimlendirilmiş bir ağaç altında toplar. Bu, thread paylaşımı ve checkpoint serileştirme için daha güvenli bir yapı sağlar.
Mühendislik Çıkarımları
- Sessiz hataları erken yakalayın: Derin öğrenme modellerinde tensor boyutu hataları uzun eğitimlerden sonra ortaya çıkabilir. Rust daha açık tip ve yapı disiplini sağlar.
- Bellek sürekliliğine dikkat edin: Transpose işlemleri belleği fiziksel olarak yeniden dizmez.
viewöncesinde contiguity kontrol edilmelidir. - Düşük çalışma zamanı yükü:
cargo build --releaseile native binary üretmek, edge sunucularda daha küçük bellek iziyle inference veya eğitim çalıştırmayı kolaylaştırır.