WASM ile Tarayıcıda Makine Öğrenmesi Modeli Çalıştırma
Makine öğrenmesi modellerini production’a almak her zaman bir baş ağrısıydı. Python runtime’ı, CUDA bağımlılıkları, container imajları… Ama son iki yıldır WASM bu denklemi köklü biçimde değiştiriyor. Bir modeli bir kez derleyip her yerde çalıştırabilmek, hem edge hem browser hem sunucu tarafında aynı binary’yi kullanmak artık hayal değil. Ben de bu yazıda size gerçek projelerde edindiğim deneyimleri aktaracağım.
WASM ile ML: Neden Bu Kadar Mantıklı?
Klasik ML deployment senaryolarını düşünün. Bir Python Flask API yazıyorsunuz, modeli yüklüyorsunuz, istek geldiğinde inference yapıp sonucu dönüyorsunuz. Bu yaklaşımın sorunları var:
- Soğuk start süresi: Python runtime’ın ayağa kalkması, modelin belleğe yüklenmesi zaman alıyor
- Bağımlılık cehennemi: numpy, scipy, torch versiyonları arasındaki uyumsuzluklar
- Edge’e taşınamama: Raspberry Pi’da, CDN edge node’unda veya tarayıcıda aynı modeli çalıştırmak başka bir proje
- Kaynak maliyeti: Her inference için tam bir Python process’i ayakta tutmak gereksiz
WASM bu sorunların üstüne gidiyor. Sandboxed çalışma ortamı, near-native performans ve platform bağımsızlığı bir arada sunuluyor. WASI (WebAssembly System Interface) ile dosya sistemi, network ve diğer sistem kaynaklarına kontrollü erişim de mümkün.
Şu an production’da kullandığım stack’i şöyle özetleyebilirim: modeller ONNX formatına convert ediliyor, ardından Rust veya C++ ile yazılmış WASM runtime’ları üzerinde çalışıyor. Hem tarayıcıda hem Cloudflare Workers gibi edge platformlarında aynı .wasm dosyası kullanılıyor.
ONNX: Ortak Dil
Farklı framework’lerden gelen modelleri WASM’da çalıştırmanın en pratik yolu ONNX formatını kullanmak. PyTorch, TensorFlow veya scikit-learn’den gelen bir modeli ONNX’e convert etmek artık çok kolay.
pip install onnx onnxruntime onnxmltools torch
Basit bir PyTorch modelini ONNX’e export edelim:
python3 << 'EOF'
import torch
import torch.nn as nn
class SentimentModel(nn.Module):
def __init__(self, input_size=768, hidden_size=256, num_classes=2):
super().__init__()
self.fc1 = nn.Linear(input_size, hidden_size)
self.relu = nn.ReLU()
self.dropout = nn.Dropout(0.3)
self.fc2 = nn.Linear(hidden_size, num_classes)
def forward(self, x):
x = self.fc1(x)
x = self.relu(x)
x = self.dropout(x)
return self.fc2(x)
model = SentimentModel()
model.eval()
dummy_input = torch.randn(1, 768)
torch.onnx.export(
model,
dummy_input,
"sentiment_model.onnx",
export_params=True,
opset_version=13,
do_constant_folding=True,
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
print("Model basariyla export edildi: sentiment_model.onnx")
EOF
Scikit-learn modellerini de ONNX’e convert edebilirsiniz:
python3 << 'EOF'
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
import numpy as np
from skl2onnx import convert_sklearn
from skl2onnx.common.data_types import FloatTensorType
X, y = make_classification(n_samples=1000, n_features=20, random_state=42)
clf = RandomForestClassifier(n_estimators=100, random_state=42)
clf.fit(X, y)
initial_type = [('float_input', FloatTensorType([None, 20]))]
onnx_model = convert_sklearn(clf, initial_types=initial_type)
with open("random_forest.onnx", "wb") as f:
f.write(onnx_model.SerializeToString())
print(f"Model boyutu: {len(onnx_model.SerializeToString()) / 1024:.1f} KB")
print("Dönüstürme tamamlandi")
EOF
WASM Runtime Seçimi
Burası kritik bir karar noktası. Kullandığınız platforma göre farklı seçenekler var:
- Wasmtime: Bytecode Alliance tarafından geliştiriliyor, WASI desteği mükemmel, sunucu tarafı için ideal
- Wasmer: Daha geniş dil desteği, PHP ve Python embeding’i var, CLI araçları güçlü
- WasmEdge: CNCF projesi, Kubernetes ile entegrasyon, ML workload’ları için optimize edilmiş
- browser WebAssembly: Native tarayıcı desteği, WASI yok ama Web API’lara tam erişim
Ben sunucu tarafı için genellikle WasmEdge tercih ediyorum çünkü ONNX runtime’ı built-in geliyor.
# WasmEdge kurulumu
curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh | bash
# ONNX plugin ile birlikte kurulum
curl -sSf https://raw.githubusercontent.com/WasmEdge/WasmEdge/master/utils/install.sh |
bash -s -- --plugins wasi_nn-onnx
# Kurulumu dogrula
wasmedge --version
wasmedge plugin list
Rust ile WASM ML Uygulaması
En performanslı sonuçları Rust’la aldım. Bir inference servisi yazalım:
cargo new --name wasm-ml-inference wasm-ml-inference
cd wasm-ml-inference
Cargo.toml dosyasını düzenleyin:
cat > Cargo.toml << 'EOF'
[package]
name = "wasm-ml-inference"
version = "0.1.0"
edition = "2021"
[dependencies]
wasi-nn = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[lib]
crate-type = ["cdylib"]
[[bin]]
name = "inference"
path = "src/main.rs"
EOF
Şimdi ana inference kodunu yazalım:
cat > src/main.rs << 'EOF'
use std::env;
use std::fs;
use wasi_nn::{
ExecutionTarget, GraphBuilder, GraphEncoding, TensorType
};
fn run_inference(model_path: &str, input_data: Vec<f32>) -> Vec<f32> {
let model_bytes = fs::read(model_path)
.expect("Model dosyasi okunamadi");
let graph = GraphBuilder::new(
GraphEncoding::Onnx,
ExecutionTarget::CPU
)
.build_from_bytes([&model_bytes])
.expect("Graf olusturulamadi");
let mut context = graph
.init_execution_context()
.expect("Context baslatılamadi");
// Input tensor ayarla
let input_shape = vec![1u32, input_data.len() as u32];
context.set_input(
0,
TensorType::F32,
&input_shape,
&input_data.iter()
.flat_map(|x| x.to_le_bytes())
.collect::<Vec<u8>>()
).expect("Input ayarlanamadi");
context.compute().expect("Inference basarisiz");
let output_bytes = context
.get_output_as_bytes(0, 2 * 4)
.expect("Output alinamadi");
output_bytes
.chunks(4)
.map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.collect()
}
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Kullanim: inference <model.onnx>");
std::process::exit(1);
}
// Örnek input: 768 boyutlu embedding vektörü
let input: Vec<f32> = (0..768)
.map(|i| (i as f32) * 0.001)
.collect();
let output = run_inference(&args[1], input);
let probs = softmax(&output);
println!("Negatif olasiligi: {:.4}", probs[0]);
println!("Pozitif olasiligi: {:.4}", probs[1]);
let predicted_class = if probs[1] > probs[0] { "Pozitif" } else { "Negatif" };
println!("Tahmin: {}", predicted_class);
}
fn softmax(logits: &[f32]) -> Vec<f32> {
let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exps: Vec<f32> = logits.iter().map(|&x| (x - max).exp()).collect();
let sum: f32 = exps.iter().sum();
exps.iter().map(|&e| e / sum).collect()
}
EOF
WASM hedefine derleyin:
rustup target add wasm32-wasi
cargo build --target wasm32-wasi --release
wasmedge --dir .:.
./target/wasm32-wasi/release/inference.wasm
sentiment_model.onnx
Tarayıcıda ML: ONNX Runtime Web
Aynı modeli tarayıcıda çalıştırmak için ONNX Runtime Web kullanıyoruz. Bu kütüphane arka planda WASM kullanıyor ve zero-dependency, download-friendly:
mkdir browser-ml-demo && cd browser-ml-demo
npm init -y
npm install onnxruntime-web
cat > index.html << 'HTMLEOF'
<!DOCTYPE html>
<html lang="tr">
<head>
<meta charset="UTF-8">
<title>WASM ML Demo</title>
<style>
body { font-family: Arial; max-width: 600px; margin: 50px auto; padding: 20px; }
button { background: #4CAF50; color: white; padding: 10px 20px;
border: none; cursor: pointer; border-radius: 4px; }
#result { margin-top: 20px; padding: 15px; background: #f0f0f0;
border-radius: 4px; display: none; }
#timing { font-size: 0.85em; color: #666; margin-top: 8px; }
</style>
</head>
<body>
<h2>Tarayici Uzerinde ML Inference</h2>
<p>Model tamamen tarayıcınızda, sunucuya veri gönderilmeden çalışıyor.</p>
<textarea id="inputText" rows="4" cols="50"
placeholder="Analiz edilecek metni girin..."></textarea>
<br><br>
<button onclick="runInference()">Analiz Et</button>
<div id="result">
<strong>Sonuc:</strong> <span id="prediction"></span>
<div id="timing"></div>
</div>
<script type="module">
import * as ort from './node_modules/onnxruntime-web/dist/esm/ort.min.js';
window.runInference = async function() {
const startTime = performance.now();
const session = await ort.InferenceSession.create('./sentiment_model.onnx', {
executionProviders: ['wasm'],
graphOptimizationLevel: 'all'
});
const inputArray = new Float32Array(768).fill(0.01);
const tensor = new ort.Tensor('float32', inputArray, [1, 768]);
const feeds = { input: tensor };
const results = await session.run(feeds);
const outputData = results.output.data;
const loadTime = (performance.now() - startTime).toFixed(1);
const expNeg = Math.exp(outputData[0]);
const expPos = Math.exp(outputData[1]);
const sumExp = expNeg + expPos;
const probPos = expPos / sumExp;
document.getElementById('prediction').textContent =
probPos > 0.5 ? 'Pozitif ✓' : 'Negatif ✗';
document.getElementById('timing').textContent =
`Inference süresi: ${loadTime}ms (model yükleme dahil)`;
document.getElementById('result').style.display = 'block';
}
</script>
</body>
</html>
HTMLEOF
npx serve . -p 8080
Edge’de Inference: Cloudflare Workers
İşin gerçekten ilginçleştiği yer burası. Bir modeli dünyanın her yerinde, kullanıcıya en yakın edge node’unda çalıştırabilmek:
npm install -g wrangler
wrangler login
wrangler init edge-ml-worker
cd edge-ml-worker
src/index.ts dosyasını düzenleyin:
cat > src/index.ts << 'EOF'
import * as ort from 'onnxruntime-web/wasm';
interface Env {
ML_MODELS: KVNamespace;
}
async function loadModel(env: Env, modelName: string): Promise<ort.InferenceSession> {
const modelData = await env.ML_MODELS.get(modelName, 'arrayBuffer');
if (!modelData) {
throw new Error(`Model bulunamadi: ${modelName}`);
}
return await ort.InferenceSession.create(modelData, {
executionProviders: ['wasm'],
graphOptimizationLevel: 'all'
});
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
if (request.method !== 'POST') {
return new Response('Method Not Allowed', { status: 405 });
}
try {
const body = await request.json() as { features: number[] };
if (!body.features || body.features.length !== 20) {
return new Response(
JSON.stringify({ error: '20 boyutlu feature vektörü gerekli' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
);
}
const startTime = Date.now();
const session = await loadModel(env, 'random_forest_v1');
const inputTensor = new ort.Tensor(
'float32',
new Float32Array(body.features),
[1, 20]
);
const results = await session.run({ float_input: inputTensor });
const prediction = results.output_label.data[0];
const inferenceTime = Date.now() - startTime;
return new Response(
JSON.stringify({
prediction: Number(prediction),
inference_time_ms: inferenceTime,
edge_location: request.cf?.colo || 'unknown',
model_version: 'random_forest_v1'
}),
{ headers: { 'Content-Type': 'application/json' } }
);
} catch (error) {
return new Response(
JSON.stringify({ error: String(error) }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
};
EOF
wrangler kv:namespace create "ML_MODELS"
wrangler kv:key put --binding=ML_MODELS "random_forest_v1" --path=./random_forest.onnx
wrangler deploy
Model Optimizasyonu: Inference Hızını Artırma
WASM ortamında model boyutu ve inference süresi kritik. Birkaç pratik optimizasyon:
python3 << 'EOF'
import onnx
from onnxruntime.transformers import optimizer
from onnxruntime.quantization import quantize_dynamic, QuantType
# Temel model optimizasyonu
model = onnx.load("sentiment_model.onnx")
optimized_model = optimizer.optimize_model(
"sentiment_model.onnx",
model_type='bert',
num_heads=8,
hidden_size=768
)
optimized_model.save_model_to_file("sentiment_model_optimized.onnx")
# INT8 quantization - model boyutunu ~4x küçültür
quantize_dynamic(
"sentiment_model_optimized.onnx",
"sentiment_model_int8.onnx",
weight_type=QuantType.QInt8,
per_channel=True,
reduce_range=True
)
import os
original_size = os.path.getsize("sentiment_model.onnx") / 1024
optimized_size = os.path.getsize("sentiment_model_optimized.onnx") / 1024
quantized_size = os.path.getsize("sentiment_model_int8.onnx") / 1024
print(f"Orijinal: {original_size:.1f} KB")
print(f"Optimize edilmis: {optimized_size:.1f} KB")
print(f"INT8 quantized: {quantized_size:.1f} KB")
print(f"Toplam küçülme: {(1 - quantized_size/original_size)*100:.1f}%")
EOF
Model boyutu WASM bundle için hayati önem taşıyor. Quantization ile hem boyutu düşürüyorsunuz hem de integer operasyonlar SIMD instruction’ları sayesinde WASM’da daha hızlı çalışıyor.
Benchmark: WASM vs Python Runtime
Gerçek dünyada ölçtüğüm bazı rakamlar:
- Cold start süresi: Python + PyTorch yaklaşık 800ms, WASM (WasmEdge) yaklaşık 45ms
- Inference latency: 768 boyutlu MLP için Python ~2ms, WASM ~2.3ms, fark %15 civarı
- Bellek kullanımı: Python process ~180MB, WASM sandbox ~28MB
- Bundle boyutu: Quantized model + WASM runtime toplamda 8-12MB arası
- Tarayıcıda ilk yükleme: 3G bağlantıda 8MB WASM bundle yaklaşık 4 saniye
Cold start farkı edge senaryolarında belirleyici oluyor. Serverless fonksiyonlarda her istek yeni bir process başlatabilir, bu noktada WASM ciddi avantaj sağlıyor.
Gerçek Dünya Kullanım Senaryoları
Şu an aktif olarak çalıştığım veya geçmişte yönettiğim birkaç senaryo:
Fraud Detection Edge’de: Bir e-ticaret müşterisinde ödeme işlemlerindeki anomali tespitini Cloudflare Workers üzerine taşıdık. Merkez sunucuya gitmeye gerek kalmadan kullanıcıya en yakın edge node’unda karar veriliyor. Latency 120ms’den 18ms’e düştü.
Tarayıcıda Dil Tespiti: Kullanıcıların yazdığı metnin dilini tespit etmek için FastText modelini WASM’a derledik. Sunucuya tek karakter gönderilmiyor, GDPR açısından da temiz.
IoT Cihazda Anomali Tespiti: Fabrika sensör verisi için WasmEdge + ONNX runtime’ı Raspberry Pi 4 üzerinde çalıştırıyoruz. Buluta bağlantı olmadan yerel inference yapılıyor.
Image Classification CDN’de: Kullanıcı yüklediği görselleri CDN edge’inde sınıflandırıp etiketliyoruz. MobileNetV2 quantized haliyle 6MB tutuyor ve edge’de 40ms’de çalışıyor.
Dikkat Edilmesi Gereken Noktalar
WASM’ı ML için kullanırken birkaç konuda gerçekçi olmak gerekiyor:
- GPU desteği yok: WASM’da GPU inference henüz yok. LLM gibi büyük modeller için WASM uygun değil. 100 milyonun üzerinde parametre olan modelleri burada çalıştırmaya çalışmayın.
- SIMD desteği: Tarayıcı tarafında WASM SIMD desteği 2021’den itibaren yaygınlaştı ama eski tarayıcılarda fallback planınız olsun.
- Threading: WASM threads SharedArrayBuffer gerektirir ve bazı proxy/CDN ortamlarında COOP/COEP header’ları ayarlanmalıdır.
- Model boyutu: Mobil cihazlarda 10MB üzeri WASM bundle’ı kullanıcı deneyimini olumsuz etkiliyor. Quantization ve pruning yapmadan deployment’a geçmeyin.
- Debugging: WASM’da hata ayıklamak hala acı verici. Source map desteği var ama production’da stack trace okumak zor.
Sonuç
WASM ile ML inference, özellikle edge computing ve tarayıcı tarafı için gerçek bir game changer. Bunu “her modeli WASM’a taşıyın” diye söylemiyorum, aksine doğru kullanım alanını bulmak şart. Küçük ve orta boyutlu modeller, latency kritik senaryolar, veri mahremiyetinin önemli olduğu durumlar ve cross-platform deployment gereksinimleri için WASM son derece mantıklı bir seçim.
Başlamak isteyenlere önerim şu: önce mevcut bir ONNX modelinizi alın, ONNX Runtime Web ile tarayıcıda çalıştırın. Bu adım nispeten kolay ve size WASM ML dünyasının nasıl hissettirdiğini gösterir. Ardından sunucu tarafı için WasmEdge’i deneyin. Production’a geçmeden önce mutlaka benchmark yapın çünkü her model ve her workload farklı davranıyor.
Rust ekosistemi burada sağlam bir temel sunuyor ama Python’dan başlamak isteyenler için wasmedge_sdk ve wasmtime Python binding’leri de mevcut. Yol uzun ama sonuç değer.
