OpenJev: LLM Kararı Artık 35 ms!
Bir uygulamanın yapay zekâdan beklediği cevap bazen tek bir boolean, birkaç seçenekten biri veya sayısal bir skordur. Böyle bir durumda modelin cevabı kelime kelime yazmasını beklemek, çözmeye çalıştığımız problem için ne kadar gerekli?
OpenJev deneyi bu soruyla başladı. Jev üzerine yaptığımız araştırmadan ilham alarak, açık ağırlıklı bir Qwen modelini yapılandırılmış kararlar üreten yerel bir sisteme dönüştürdük. Başlangıç hedefimiz, RTX 4060 Ti 16 GB üzerinde P50 gecikmeyi 70 ms, P95 gecikmeyi 100 ms altında tutmaktı.
İlk prototipte, 37 token’lık bir girdiden dört karar üreten gerçek localhost HTTP çağrılarında P50 34,6 ms ve P95 50,5 ms ölçtük. Bunlar model belleğe yüklendikten sonra, 100 ardışık istek üzerinden alınan sonuçlar.
Bu yazıda o sonuca nasıl ulaştığımızı, gerçekte neyi eğittiğimizi ve henüz neyi çözmediğimizi anlatacağım.
Metin üretiminden karar üretimine
Ödeme işlemini değerlendiren bir uygulama düşünelim. Uygulama aynı işlem için şu çıktıları istiyor:
- İşlem şüpheli mi?
- Ek doğrulama gerekli mi?
- İşlem hangi akışa yönlendirilmeli?
- Risk skoru ne?
Üretken bir dil modelinden bunları JSON olarak yazmasını isteyebiliriz. Ancak bu kullanımda model, alan adlarından noktalama işaretlerine kadar çıktı metnini token token üretir. Ardından uygulama bu metni ayrıştırır.
OpenJev’de akışı şöyle kurduk:
JSON durum bilgisi
↓
Tokenizer
↓
Qwen — tek forward pass
↓
Ortak temsil
↓
Boolean / Choice / Score başlıkları
↓
Sayısal çıktılar
↓
Uygulamanın oluşturduğu JSON yanıtı
Modelin işi sayıları hesaplamak. JSON nesnesini oluşturmak ise uygulama kodunun işi. Böylece çıktı token’larını sırayla üreten döngüye ihtiyaç kalmıyor.
Bu yaklaşımın faydasını değerlendirirken iki ayrımı korumak gerekiyor. Birincisi, geçerli bir JSON yanıtı üretmek doğru karar vermek anlamına gelmiyor. İkincisi, aynı makinede autoregressive bir alternatifle karşılaştırmalı benchmark yapmadık. Bu yüzden bu çalışma için belirli bir “kat hızlanma” iddiasında bulunmuyoruz; ölçtüğümüz şey kendi karar hattımızın gecikmesi.
Neden Qwen2.5-0.5B?
Başlangıçta Qwen2.5-0.5B ve Qwen3.5-0.8B seçeneklerini değerlendirdik. İlk sürüm için Qwen2.5-0.5B seçtik. Küçük bir modelle başlayıp mimariyi, eğitim hattını ve gecikmeyi ölçmek istiyorduk.
Bu bir model yarışı değildi. Qwen3.5-0.8B’yi bu deneyde indirip çalıştırmadık; sonuçlardan iki model arasında performans karşılaştırması çıkarılamaz.
Qwen2.5-0.5B ağırlıklarını ve tokenizer dosyalarını proje içine indirdik. Tekrar üretilebilirlik için indirdiğimiz modelin revision bilgisini de kaydettik:
Model: Qwen/Qwen2.5-0.5B
Revision: 060db6499f32faf8b98477b0a26969ef7d8b9987
Çalışma sırasında model yerel dosyalardan yükleniyor; inference için harici bir model API’sine istek gönderilmiyor.
Qwen’in hangi kısmını değiştirdik?
Buradaki teknik ayrım önemli: Qwen’in decoder transformer katmanlarını koruduk. Causal attention yapısını çift yönlü bir encoder’a çevirmedik.
Dil modeli çıktı başlığı yerine, transformer’ın gizli durumlarını döndüren AutoModel kullandık. Son görünür token’ın temsilini alıp kendi karar katmanlarımıza bağladık. Causal yapıda bu konum, kendisinden önceki tüm girdi token’larına erişebiliyor. Padding eklenmiş girdilerde de gerçek son token’ı attention mask üzerinden seçiyoruz.
Kurduğumuz yapı şu şekilde:
JSON state
↓
Anahtarları sıralanmış, tutarlı metin gösterimi
↓
Qwen tokenizer
↓
Qwen2Model — FP16, SDPA, use_cache=False
↓
Son görünür token: 896 boyut
↓
LayerNorm → Linear(896, 512) → GELU
↓
├── fraud: Linear(512, 1) → sigmoid
├── require_3ds: Linear(512, 1) → sigmoid
├── route: Linear(512, 3) → softmax
└── risk: Linear(512, 1) → sigmoid
fraud ve require_3ds başlıkları boolean kararları, route üç seçenek üzerindeki olasılık dağılımını, risk ise 0–1 aralığında bir regresyon değerini temsil ediyor.
Dört görev de aynı Qwen temsilini kullanıyor. Her görev için modeli yeniden çalıştırmıyoruz. “Paralel karar başlıkları” derken kastımız bu ortak hesaplama: uygulamada küçük lineer başlıklar ayrı ayrı çağrılıyor; farklı CUDA stream’lerinde eşzamanlı çalıştıklarını iddia etmiyoruz.
Risk skoru ile sınıflandırma olasılığını da ayırıyoruz. risk=0.8, modelin cevabından yüzde 80 emin olduğu anlamına gelmiyor. Bu, eğitimde tanımlanan sayısal hedefin tahmini.
Modeli indirmek yetmedi: başlıkları eğittik
Hazır Qwen ağırlıkları, sonradan eklediğimiz fraud veya route başlıklarını kendiliğinden anlamlı hâle getirmiyor. Yeni katmanlar başlangıçta rastgele ağırlıklara sahip. Bu nedenle ilk sürüme eğitim hattını da ekledik.
Qwen’in ağırlıklarını dondurduk. Eğitim örneklerinin temsillerini bir kez çıkardık; yalnızca ortak projeksiyonu ve karar başlıklarını FP32 olarak eğittik. Böylece her epoch’ta büyük modeli yeniden çalıştırmamız gerekmedi.
Her çıktı türüne uygun bir kayıp fonksiyonu kullandık:
| Çıktı türü | Eğitim kaybı |
|---|---|
| Boolean | Binary cross-entropy |
| Choice | Cross-entropy |
| Score | Normalize edilmiş hedef üzerinde mean squared error |
İlk veri kümesi 720 sentetik örnekten oluşuyordu: 480 eğitim, 120 doğrulama ve 120 test örneği. İşlem tutarı, ülke farkı, başarısız deneme sayısı ve hesap yaşı gibi alanlardan basit bir politika ile etiket ürettik. Aynı state’in farklı veri bölümlerinde bulunmasını reddeden kontrol de ekledik.
Bu veriler gerçek dolandırıcılık etiketleri değildi. Amacımız, girdiden eğitime ve API yanıtına kadar bütün hattın çalıştığını görmekti. Hatta bu sentetik etiketleri üreten kurallar zaten elimizde olduğu için, yalnızca bu oyuncak problemi çözmek isteseydik onları doğrudan uygulamak yeterli olurdu. Transformer kullanmanın değerini göstermek için daha sonra gerçek, daha karmaşık görevlerle ve uygun basit modellerle karşılaştırma yapmak gerekiyor.
120 epoch içinden doğrulama kaybı en düşük olan 118. epoch seçildi. Ayrı tutulan test kümesindeki sonuçlar şöyleydi:
| Görev | Test sonucu |
|---|---|
| fraud | %87,5 doğruluk |
| require_3ds | %77,5 doğruluk |
| route | %86,7 doğruluk |
| risk | 0,084 ortalama mutlak hata |
Bu tablo, modelin sentetik politikayı ne ölçüde öğrendiğini gösteriyor. Gerçek ödeme işlemlerindeki başarıyı göstermiyor.
Kalibrasyon eklemek, kalibrasyon problemini bitirmiyor
Otomasyon sistemlerinde yalnızca seçilen sınıf değil, o sınıfa verilen olasılık da önemli. Örneğin 0,99 olasılıkla verilen yanlış bir karar, bir güven eşiğinin davranışını ciddi biçimde değiştirebilir.
Bu yüzden boolean ve choice başlıklarına temperature scaling ekledik. Modelin sigmoid veya softmax öncesindeki skorlarını bir sıcaklık değeriyle ölçekliyoruz:
Boolean: sigmoid(z / T)
Choice: softmax(z / T)
Her başlık için sıcaklığı doğrulama kümesindeki negatif log-olabilirlik kaybına, yani NLL’ye göre seçtik. Score başlığına bu işlemi uygulamadık; o başlık bir regresyon hedefini tahmin ediyor.
Deneyin öğretici taraflarından biri, kalibrasyonun test kümesinde her zaman iyileşme getirmemesi oldu. require_3ds için test NLL değeri 0,413’ten 0,625’e, olasılıklarla doğruluk arasındaki uyumsuzluğu özetleyen ECE ise 0,058’den 0,109’a yükseldi. Her iki ölçütte de düşük değer daha iyi.
Dolayısıyla API’deki calibrated=true, kalibrasyon işleminin uygulanmış olduğunu söylüyor. Yeni verilerde olasılıkların güvenilir olduğuna dair bir garanti vermiyor. Bu küçük deneyde bile yalnızca doğruluk oranına bakmanın neden yetersiz kaldığını gördük.
70–100 ms hedefini nasıl ölçtük?
İlk ölçümleri 16 Eylül 2026’da şu ortamda yaptık:
| Bileşen | Yapılandırma |
|---|---|
| GPU | NVIDIA RTX 4060 Ti, 16 GB |
| İşletim sistemi | Windows |
| PyTorch | 2.7.1+cu128 |
| Transformers | 4.51.3 |
| Backbone hassasiyeti | FP16 |
| Attention | SDPA |
| Batch size | 1 |
| Karar başlığı | 4 |
Her ölçüm grubunda 20 ısınma çalıştırmasından sonra 100 ardışık örnek topladık. Uygulama içi ölçümlerde CUDA işlemlerinin bitmesini bekleyerek yalnızca GPU’ya iş gönderme süresini ölçmekten kaçındık.
İki farklı süreyi ayrı kaydettik. Uygulama ölçümü, state’in metne çevrilmesini, tokenizer’ı, cihaz transferlerini, Qwen’i, başlıkları ve JSON yanıtının oluşturulmasını kapsıyor. Gerçek HTTP ölçümü ise çalışan sunucuya localhost üzerinden gönderilen TCP isteklerini istemci tarafından ölçüyor; bağlantı yeniden kullanılıyor.
P50 medyan gecikmeyi, P95 ise ölçümlerin yaklaşık yüzde 95’inin altında kaldığı süreyi ifade ediyor.
| Ölçüm | Girdi token sayısı | P50 | P95 |
|---|---|---|---|
| Gerçek localhost HTTP, örnek işlem | 37 | 34,62 ms | 50,52 ms |
| Uygulama, örnek işlem | 37 | 33,08 ms | 46,06 ms |
| Uygulama, sentetik metin | 64 | 33,27 ms | 49,12 ms |
| Uygulama, sentetik metin | 128 | 36,51 ms | 76,63 ms |
| Uygulama, sentetik metin | 256 | 36,48 ms | 48,14 ms |
| Uygulama, sentetik metin | 512 | 32,35 ms | 46,37 ms |
Bu koşullarda başlangıç hedefimizi karşıladık. PyTorch’un kaydettiği tepe GPU allocation değeri de yaklaşık 978 MiB oldu; bu sayı masaüstünün ve diğer uygulamaların toplam GPU kullanımını içermiyor.
Tablodaki 512 token sonucunun bazı kısa girdilerden daha hızlı görünmesi, uzun girdilerin genel olarak daha hızlı olduğu anlamına gelmiyor. Ayrı zamanlarda alınmış kısa ölçüm serileri görüyoruz; GPU saatleri ve arka plan yükü gibi etkenler sonucu etkileyebilir.
Ölçümlerin kapsamı da belli: model yükleme ve soğuk başlangıç dahil değil. Uzun girdiler tekrar eden sentetik metinlerden oluşuyor. HTTP ölçümü 37 token’lık örneğe ait. İnternet gecikmesini, eşzamanlı kullanıcıları veya 32 başlıklı bir yapılandırmayı test etmedik. Sunucu inference işlemlerini kilitle sıraya aldığı için, yük arttığında kuyruk bekleme süresi ayrıca değerlendirilmek zorunda.
Prototipi API üzerinden çağırmak
Karar motorunu FastAPI ile servis ettik. Kurulum ve demo eğitimi tamamlanmış proje klasöründe sunucu şu komutla başlıyor:
.venv/Scripts/python.exe -m openjev serve
Ardından aşağıdaki isteği gönderebiliyoruz. Aynı cURL, Postman’in Import alanına da yapıştırılabilir:
curl --request POST 'http://127.0.0.1:8000/decide' \
--header 'Content-Type: application/json' \
--data-raw '{
"state": {
"amount": 32500,
"merchant_country": "TR",
"card_country": "DE",
"failed_attempts": 3,
"account_age_days": 12
}
}'
Yanıtta decisions altında dört görevin çıktısı yer alıyor. Boolean görevlerde değer ve pozitif sınıf olasılığı, choice görevinde seçeneklerin olasılıkları, score görevinde ise sayısal tahmin dönüyor. Yukarıdaki isteğin state alanını yerel demo checkpoint’i ile yeniden çalıştırdığımızda aldığımız tam JSON yanıtı şöyle:
{
"decisions": {
"fraud": {
"type": "boolean",
"value": true,
"probability": 0.7277871966362,
"calibrated": true
},
"require_3ds": {
"type": "boolean",
"value": true,
"probability": 0.9999990463256836,
"calibrated": true
},
"route": {
"type": "choice",
"value": "manual_review",
"probabilities": {
"domestic": 0.000423165678512305,
"international": 0.2556307315826416,
"manual_review": 0.7439460754394531
},
"calibrated": true
},
"risk": {
"type": "score",
"value": 0.6240540146827698
}
},
"metadata": {
"trained": true,
"data_kind": "synthetic_demo",
"input_tokens": 37,
"backbone_passes": 1,
"generated_tokens": 0
}
}
Bu örnekte fraud başlığının pozitif sınıf olasılığı yaklaşık %72,8; seçilen yönlendirme manual_review, risk skoru ise yaklaşık 0,624. Bunlar sentetik veriyle eğitilmiş demo modelinin tahminleri. Özellikle require_3ds için 1’e çok yakın çıkan olasılık, gerçek dünyada aynı düzeyde doğruluk garantisi vermiyor; kalibrasyon bölümünde bu başlığın test sonuçlarını ayrıca ele aldık.
metadata, yanıtın tek backbone geçişiyle ve hiç çıktı token’ı üretilmeden oluşturulduğunu da gösteriyor. Yukarıdaki JSON metnini model kelime kelime yazmıyor; uygulama, başlıkların sayısal çıktılarını bu yapıya dönüştürüyor.
API’ye birkaç temel kontrol ekledik. Eğitilmemiş başlıklarla normal tahmin yapılmıyor. Varsayılan 512 token sınırını aşan girdi sessizce kesilmiyor; hata dönüyor. Checkpoint’in görev şeması ve model revision bilgisi yüklenen yapılandırmayla karşılaştırılıyor.
İlk doğrulama çalıştırmasında 17 test geçti. Bunların arasında gerçek Qwen ile tek backbone çağrısının kontrolü, çıktı türleri, checkpoint kaydetme/yükleme, padding davranışı, uzun girdi reddi ve API testleri bulunuyor.
İlk sürümün sınırı ve sonraki deney
OpenJev v0.1, önceden tanımlanmış görevleri tek model geçişinde hesaplayan bir prototip. İstek sırasında yeni bir doğal dil sorusu yazıp, sistemin otomatik olarak yeni bir görev öğrenmesini bekleyemiyoruz. Görevler ve seçenekler eğitimde belirleniyor; değiştiklerinde yeniden eğitim gerekiyor.
Kodda mevcut öğretmen çıktılarıyla soft-target distillation yapmaya yarayan bir yol da var. Ancak bu deneyde öğretmen model kullanmadık. Henüz LoRA, backbone fine-tuning, dinamik soru/seçenek encoder’ları, early exit, quantization veya TensorRT uygulamadık. Jev’in özel mimarisini ya da RLCD algoritmasını yeniden ürettiğimiz iddiasında da bulunmuyoruz.
Bundan sonraki anlamlı adım, gerçek bir görev veri kümesi seçmek. O veri üzerinde basit kural sistemleri, geleneksel sınıflandırıcılar ve üretken bir model yaklaşımıyla hem kaliteyi hem gecikmeyi karşılaştırmak gerekiyor. Daha geniş test kümeleri ve eşzamanlı yük ölçümleri, prototipten kullanılabilir bir servise geçişin diğer parçaları.
Bu deneyde ortaya koyduğumuz somut sonuç şu: küçük bir Qwen backbone’u, ortak bir temsil ve birkaç öğrenilmiş başlıkla, tek geçişte yapılandırılmış karar üreten yerel bir sistem kurabildik. Ölçtüğümüz kısa ve sıcak iş yükünde gecikme hedefini karşıladık. Şimdi asıl çalışma, aynı hattın doğru kararları ne kadar güvenilir biçimde verebildiğini göstermek.
Model kaynağı: Qwen2.5-0.5B resmi model kartı. Yazıdaki deney sonuçları, projenin 16 Eylül 2026 tarihli reports/benchmark.json, reports/http-benchmark.json ve checkpoints/demo/metadata.json kayıtlarına dayanıyor.