Switch the project language to English
CI / test (push) Has been cancelled

Comments, doc comments, test names, error and log messages, CLI help, README,
Makefile, CI step names and the design document are now all in English.
No behaviour changes; the full suite passes under the race detector.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ilkerandClaude Opus 5 committed 2026-09-16 01:15:21 +02:00
1 parent b7417e1a69
commit 28029eb43b
21 files changed
+910 -869

No files matched your search

+3 -3
View File
@@ -16,15 +16,15 @@ jobs:
go-version: "1.26" go-version: "1.26"
cache: true cache: true
- name: Biçim kontrolü - name: Check formatting
run: | run: |
out=$(gofmt -l .) out=$(gofmt -l .)
if [ -n "$out" ]; then echo "gofmt gerekli:"; echo "$out"; exit 1; fi if [ -n "$out" ]; then echo "gofmt needed on:"; echo "$out"; exit 1; fi
- name: Vet - name: Vet
run: go vet ./... run: go vet ./...
- name: Test (race detector ile) - name: Test with the race detector
run: go test -race ./... run: go test -race ./...
- name: Build - name: Build
+1 -1
View File
@@ -20,7 +20,7 @@ linters:
exclusions: exclusions:
rules: rules:
# Testlerde okunabilirlik uğruna gevşetilenler. # Relaxed in tests for readability.
- path: _test\.go - path: _test\.go
linters: [bodyclose, noctx, errorlint] linters: [bodyclose, noctx, errorlint]
+2 -2
View File
@@ -23,9 +23,9 @@ fmt:
clean: clean:
rm -rf bin dist rm -rf bin dist
# Modül yolunu değiştirmek için: make rename NEW=git.ornek.com/ekip/llmgw # Change the module path: make rename NEW=git.example.com/team/llmgw
rename: rename:
@test -n "$(NEW)" || (echo "kullanim: make rename NEW=<yeni/modul/yolu>"; exit 1) @test -n "$(NEW)" || (echo "usage: make rename NEW=<new/module/path>"; exit 1)
go mod edit -module=$(NEW) go mod edit -module=$(NEW)
grep -rl '$(MODULE)' --include='*.go' . | xargs sed -i '' 's|$(MODULE)|$(NEW)|g' grep -rl '$(MODULE)' --include='*.go' . | xargs sed -i '' 's|$(MODULE)|$(NEW)|g'
sed -i '' 's|^MODULE := .*|MODULE := $(NEW)|' Makefile sed -i '' 's|^MODULE := .*|MODULE := $(NEW)|' Makefile
+44 -33
View File
@@ -1,58 +1,62 @@
# llmgw # llmgw
Çoklu InfiniBand fabric ve heterojen GPU için tek binary LLM gateway. A single-binary LLM gateway for clusters with multiple InfiniBand fabrics and
heterogeneous GPUs.
Kendi GPU sunucularında çalışan LLM'leri (vLLM / SGLang) tek bir OpenAI uyumlu It serves LLMs running on your own GPU servers (vLLM / SGLang) behind one
uç noktadan sunar. Tasarım dokümanı: [llmgw-plan.md](llmgw-plan.md). OpenAI-compatible endpoint. Design document: [llmgw-plan.md](llmgw-plan.md).
## Durum ## Status
| Faz | Kapsam | Durum | | Phase | Scope | State |
|---|---|---| |---|---|---|
| 0 | İskelet + sim-engine | ✅ | | 0 | Skeleton and sim-engine | done |
| 1 | Statik router (filter/score/pick, streaming proxy) | ⬜ | | 1 | Static router (filter/score/pick, streaming proxy) | in progress |
| 1.5 | Admission control (öncelik bantları) | ⬜ | | 1.5 | Admission control (priority bands) | todo |
| 2 | Agent: GPU/IB keşfi, tek-node engine | ⬜ | | 2 | Agent: GPU/IB discovery, single-node engines | todo |
| 3 | Control plane: katalog, scheduler, reconciler | ⬜ | | 3 | Control plane: catalog, scheduler, reconciler | todo |
| 4 | Multi-node & fabric farkındalığı | ⬜ | | 4 | Multi-node placement and fabric awareness | todo |
| 5 | Tenancy, prefix affinity, bench | ⬜ | | 5 | Tenancy, prefix affinity, benchmarking | todo |
## Kurulum ## Building
```sh ```sh
make build # bin/llmgw make build # bin/llmgw
make test # birim testleri make test # unit tests
make race # yarış dedektörüyle make race # with the race detector
``` ```
## sim-engine ## sim-engine
Gerçek bir inference engine'in **router açısından önemli** davranışlarını simüle Simulates the inference-engine behaviours **the router cares about**, so the
eder; GPU olmadan geliştirme ve test yapabilmek için. Modellediği şeyler: router can be developed and tested without GPUs. What it models:
- continuous batching (sınırlı eşzamanlı dizi sayısı) - continuous batching (bounded number of concurrent sequences)
- KV cache blok muhasebesi; blok yetmediğinde kuyruğa alma (head-of-line blocking dahil) - KV cache block accounting, queueing once blocks run out (head-of-line
- otomatik prefix cache (zincirlenmiş blok hash'leri + LRU) ve cache hit'in TTFT'ye etkisi blocking included)
- batch büyüdükçe token başına gecikmenin artması - automatic prefix caching (chained block hashes plus an LRU) and its effect
- istemci koptuğunda isteğin batch'ten düşürülüp blokların bırakılması on time to first token
- vLLM metrik adlarıyla `/metrics` - per-token latency growing with batch size
- dropping a request and freeing its blocks when the client disconnects
- `/metrics` using vLLM's metric names
```sh ```sh
./bin/llmgw sim-engine --listen :8000 --model llama-3.3-70b ./bin/llmgw sim-engine --listen :8000 --model llama-3.3-70b
curl -N localhost:8000/v1/chat/completions \ curl -N localhost:8000/v1/chat/completions \
-H 'content-type: application/json' \ -H 'content-type: application/json' \
-d '{"messages":[{"role":"user","content":"merhaba"}],"max_tokens":8,"stream":true}' -d '{"messages":[{"role":"user","content":"hello"}],"max_tokens":8,"stream":true}'
``` ```
Yük/gecikme davranışını ayarlamak için: `--max-num-seqs`, `--num-gpu-blocks`, Tune load and latency behaviour with `--max-num-seqs`, `--num-gpu-blocks`,
`--prefill-tps`, `--decode-tps`, `--batch-contention`, `--prefix-cache-blocks`. `--prefill-tps`, `--decode-tps`, `--batch-contention` and
Kaos testleri için: `--fail-rate`, deterministik koşu için `--seed`. `--prefix-cache-blocks`. For chaos testing use `--fail-rate`, and `--seed` for
deterministic runs.
### Engine metrik kontratı ### The engine metric contract
Router'ın yönlendirme yapabilmesi için bir engine'in vermesi **zorunlu** üç metrik For the router to make routing decisions an engine **must** expose these three
(plan §7.6). sim-engine bunları gerçek vLLM adlarıyla yayınlar: metrics (plan §7.6). sim-engine publishes them under vLLM's real names:
``` ```
vllm:num_requests_running vllm:num_requests_running
@@ -60,10 +64,17 @@ vllm:num_requests_waiting
vllm:kv_cache_usage_perc vllm:kv_cache_usage_perc
``` ```
## Modül yolu Adding support for a new engine means satisfying that contract. Metric names
drift between versions, so the name mapping is configurable.
`gitea.mynodes.xyz/ilker/llmgw`. Değiştirmek için: ## Module path
`gitea.mynodes.xyz/ilker/llmgw`. To change it:
```sh ```sh
make rename NEW=git.ornek.com/ekip/llmgw make rename NEW=git.example.com/team/llmgw
``` ```
## Language
Code, comments, tests, commit messages and documentation are in English.
+3 -3
View File
@@ -1,5 +1,5 @@
// Command llmgw, tek binary halinde router, control plane, agent ve yardımcı // Command llmgw bundles the router, control plane, agent and supporting tools
// araçları barındırır. Hangi rolde çalışacağı alt komutla belirlenir. // into a single binary. The subcommand selects which role it runs as.
package main package main
import ( import (
@@ -11,7 +11,7 @@ import (
func main() { func main() {
if err := cli.Root().Execute(); err != nil { if err := cli.Root().Execute(); err != nil {
fmt.Fprintln(os.Stderr, "hata:", err) fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1) os.Exit(1)
} }
} }
+7 -7
View File
@@ -1,4 +1,4 @@
// Package cli, llmgw'nin komut satırı yüzeyini kurar. // Package cli builds llmgw's command line surface.
package cli package cli
import ( import (
@@ -17,13 +17,13 @@ type globalFlags struct {
var global globalFlags var global globalFlags
// Root, kök komutu ve tüm alt komutları döner. // Root returns the root command with all subcommands attached.
func Root() *cobra.Command { func Root() *cobra.Command {
root := &cobra.Command{ root := &cobra.Command{
Use: "llmgw", Use: "llmgw",
Short: "Çoklu IB fabric ve heterojen GPU için LLM gateway", Short: "LLM gateway for multi-fabric InfiniBand and heterogeneous GPU clusters",
Long: "llmgw, kendi GPU sunucularında çalışan LLM'leri tek bir OpenAI uyumlu\n" + Long: "llmgw serves LLMs running on your own GPU servers behind a single\n" +
"uç noktadan sunar. Tek binary, alt komutla farklı roller.", "OpenAI-compatible endpoint. One binary, different roles per subcommand.",
Version: version.Version, Version: version.Version,
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: true, SilenceErrors: true,
@@ -33,8 +33,8 @@ func Root() *cobra.Command {
} }
pf := root.PersistentFlags() pf := root.PersistentFlags()
pf.StringVar(&global.logLevel, "log-level", "info", "log seviyesi: debug|info|warn|error") pf.StringVar(&global.logLevel, "log-level", "info", "log level: debug|info|warn|error")
pf.StringVar(&global.logFormat, "log-format", "text", "log biçimi: text|json") pf.StringVar(&global.logFormat, "log-format", "text", "log format: text|json")
root.AddCommand( root.AddCommand(
newServerCmd(), newServerCmd(),
+19 -19
View File
@@ -20,10 +20,10 @@ func newSimEngineCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "sim-engine", Use: "sim-engine",
Short: "OpenAI uyumlu sahte inference engine (test için)", Short: "OpenAI-compatible fake inference engine (for testing)",
Long: "Gerçek bir engine'in router açısından önemli davranışlarını simüle eder:\n" + Long: "Simulates the engine behaviours that matter to the router:\n" +
"continuous batching, KV blok muhasebesi, prefix cache, vLLM formatında /metrics.\n" + "continuous batching, KV block accounting, prefix caching, and vLLM-shaped\n" +
"Router'ı GPU olmadan test etmek için kullanılır.", "/metrics. Lets the router be developed and tested without GPUs.",
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
log := logger() log := logger()
eng := sim.New(cfg) eng := sim.New(cfg)
@@ -39,7 +39,7 @@ func newSimEngineCmd() *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
log.Info("sim-engine dinliyor", log.Info("sim-engine listening",
"addr", ln.Addr().String(), "addr", ln.Addr().String(),
"model", cfg.Model, "model", cfg.Model,
"max_num_seqs", cfg.MaxNumSeqs, "max_num_seqs", cfg.MaxNumSeqs,
@@ -60,7 +60,7 @@ func newSimEngineCmd() *cobra.Command {
} }
return err return err
case <-ctx.Done(): case <-ctx.Done():
log.Info("kapanıyor") log.Info("shutting down")
shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() defer cancel()
return srv.Shutdown(shutCtx) return srv.Shutdown(shutCtx)
@@ -69,19 +69,19 @@ func newSimEngineCmd() *cobra.Command {
} }
f := cmd.Flags() f := cmd.Flags()
f.StringVar(&listen, "listen", ":8000", "dinlenecek adres") f.StringVar(&listen, "listen", ":8000", "address to listen on")
f.StringVar(&cfg.Model, "model", cfg.Model, "sunulan model adı") f.StringVar(&cfg.Model, "model", cfg.Model, "model name to serve")
f.IntVar(&cfg.MaxNumSeqs, "max-num-seqs", cfg.MaxNumSeqs, "eşzamanlı dizi sayısı (batch kapasitesi)") f.IntVar(&cfg.MaxNumSeqs, "max-num-seqs", cfg.MaxNumSeqs, "concurrent sequences (continuous batch capacity)")
f.IntVar(&cfg.BlockSize, "block-size", cfg.BlockSize, "KV blok başına token") f.IntVar(&cfg.BlockSize, "block-size", cfg.BlockSize, "tokens per KV cache block")
f.IntVar(&cfg.NumGPUBlocks, "num-gpu-blocks", cfg.NumGPUBlocks, "toplam KV cache bloğu") f.IntVar(&cfg.NumGPUBlocks, "num-gpu-blocks", cfg.NumGPUBlocks, "total KV cache blocks")
f.IntVar(&cfg.PrefixCacheBlocks, "prefix-cache-blocks", cfg.PrefixCacheBlocks, "prefix cache blok kapasitesi (0=kapalı)") f.IntVar(&cfg.PrefixCacheBlocks, "prefix-cache-blocks", cfg.PrefixCacheBlocks, "prefix cache capacity in blocks (0 disables)")
f.Float64Var(&cfg.PrefillTokensPerSec, "prefill-tps", cfg.PrefillTokensPerSec, "prefill hızı (token/sn, engine geneli)") f.Float64Var(&cfg.PrefillTokensPerSec, "prefill-tps", cfg.PrefillTokensPerSec, "prefill rate in tokens/sec, shared engine-wide")
f.Float64Var(&cfg.DecodeTokensPerSec, "decode-tps", cfg.DecodeTokensPerSec, "tek dizi decode hızı (token/sn)") f.Float64Var(&cfg.DecodeTokensPerSec, "decode-tps", cfg.DecodeTokensPerSec, "single-sequence decode rate in tokens/sec")
f.Float64Var(&cfg.BatchContention, "batch-contention", cfg.BatchContention, "batch'teki her ek dizinin yavaşlatma katsayısı") f.Float64Var(&cfg.BatchContention, "batch-contention", cfg.BatchContention, "per-extra-sequence decode slowdown factor")
f.IntVar(&cfg.DefaultOutputTokens, "default-output-tokens", cfg.DefaultOutputTokens, "max_tokens verilmezse üretilecek token") f.IntVar(&cfg.DefaultOutputTokens, "default-output-tokens", cfg.DefaultOutputTokens, "tokens generated when max_tokens is absent")
f.DurationVar(&cfg.Tick, "tick", cfg.Tick, "simülasyon adımı") f.DurationVar(&cfg.Tick, "tick", cfg.Tick, "simulation step")
f.Float64Var(&cfg.FailRate, "fail-rate", 0, "isteklerin yapay hata oranı (kaos testi)") f.Float64Var(&cfg.FailRate, "fail-rate", 0, "fraction of requests failed artificially (chaos testing)")
f.Int64Var(&cfg.Seed, "seed", 0, "rastgelelik tohumu (0=zaman)") f.Int64Var(&cfg.Seed, "seed", 0, "random seed (0 = time-based)")
return cmd return cmd
} }
+12 -12
View File
@@ -6,19 +6,19 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
// errNotImplemented, henüz yazılmamış fazların komutları için. // errNotImplemented marks commands belonging to phases not yet built.
var errNotImplemented = errors.New("bu komut henüz uygulanmadı (bkz. llmgw-plan.md §11)") var errNotImplemented = errors.New("not implemented yet (see llmgw-plan.md §11)")
func newServerCmd() *cobra.Command { func newServerCmd() *cobra.Command {
var configPath string var configPath string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "server", Use: "server",
Short: "Router + control plane (Faz 1-3)", Short: "Router and control plane (phases 1-3)",
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
return errNotImplemented return errNotImplemented
}, },
} }
cmd.Flags().StringVar(&configPath, "config", "server.yaml", "konfigürasyon dosyası") cmd.Flags().StringVar(&configPath, "config", "server.yaml", "configuration file")
return cmd return cmd
} }
@@ -26,26 +26,26 @@ func newAgentCmd() *cobra.Command {
var configPath string var configPath string
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "agent", Use: "agent",
Short: "GPU node'unda çalışan agent (Faz 2)", Short: "Node agent running on each GPU host (phase 2)",
RunE: func(_ *cobra.Command, _ []string) error { RunE: func(_ *cobra.Command, _ []string) error {
return errNotImplemented return errNotImplemented
}, },
} }
cmd.Flags().StringVar(&configPath, "config", "agent.yaml", "konfigürasyon dosyası") cmd.Flags().StringVar(&configPath, "config", "agent.yaml", "configuration file")
return cmd return cmd
} }
func newCtlCmd() *cobra.Command { func newCtlCmd() *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "ctl", Use: "ctl",
Short: "Yönetim CLI'ı (Faz 3)", Short: "Administrative CLI (phase 3)",
} }
for _, sub := range []struct{ use, short string }{ for _, sub := range []struct{ use, short string }{
{"nodes", "node'ları listele"}, {"nodes", "list nodes"},
{"models", "model kataloğunu göster"}, {"models", "show the model catalog"},
{"replicas", "replikaları listele"}, {"replicas", "list replicas"},
{"explain-placement", "bir modelin neden yerleştirilemediğini açıkla"}, {"explain-placement", "explain why a model could not be placed"},
{"bench", "profil kapasitesini ölç (§10)"}, {"bench", "measure profile capacity (§10)"},
} { } {
cmd.AddCommand(&cobra.Command{ cmd.AddCommand(&cobra.Command{
Use: sub.use, Use: sub.use,
+3 -3
View File
@@ -1,4 +1,4 @@
// Package obs, log ve metrik kurulumunu tek yerden yapar. // Package obs sets up logging and metrics in one place.
package obs package obs
import ( import (
@@ -7,13 +7,13 @@ import (
"strings" "strings"
) )
// Options, log kurulumunu belirler. // Options controls logger setup.
type Options struct { type Options struct {
Level string // debug, info, warn, error Level string // debug, info, warn, error
Format string // text, json Format string // text, json
} }
// Setup, slog varsayılan logger'ını kurar ve onu döner. // Setup installs the default slog logger and returns it.
func Setup(o Options) *slog.Logger { func Setup(o Options) *slog.Logger {
var lvl slog.Level var lvl slog.Level
switch strings.ToLower(o.Level) { switch strings.ToLower(o.Level) {
+22 -22
View File
@@ -6,17 +6,17 @@ import (
"hash/fnv" "hash/fnv"
) )
// BlockHash, KV cache'in muhasebe birimi olan bir bloğun kimliği. // BlockHash identifies one KV cache block, the unit of cache accounting.
type BlockHash uint64 type BlockHash uint64
// BlockHashes, token dizisi için zincirlenmiş blok hash'leri üretir. // BlockHashes computes chained block hashes over a token sequence.
// //
// Zincirleme (h_i = hash(h_{i-1}, block_i)) vLLM'in yaptığının aynısıdır ve // Chaining (h_i = hash(h_{i-1}, block_i)) mirrors what vLLM does and is
// şart: blok kimliği sadece kendi içeriğini değil, kendisinden önce gelen tüm // required: a block's identity must represent not only its own contents but
// blokları da temsil etmeli. Aksi halde "aynı cümleyi farklı bağlamda içeren" // every block before it. Otherwise two requests containing the same sentence
// iki istek yanlışlıkla cache hit sayılır. // in different contexts would falsely count as a cache hit.
// //
// Son eksik blok yok sayılır — yarım blok cache'lenmez. // A trailing partial block is ignored — half blocks are never cached.
func BlockHashes(tokens []Token, blockSize int) []BlockHash { func BlockHashes(tokens []Token, blockSize int) []BlockHash {
if blockSize <= 0 || len(tokens) < blockSize { if blockSize <= 0 || len(tokens) < blockSize {
return nil return nil
@@ -39,13 +39,12 @@ func BlockHashes(tokens []Token, blockSize int) []BlockHash {
return out return out
} }
// PrefixCache, engine'in otomatik prefix cache'ini modelleyen blok bazlı LRU. // PrefixCache models an engine's automatic prefix cache as a block-level LRU.
// //
// Gerçek engine'de bu bir radix tree'dir; ama yönlendirme kalitesini ölçmek için // A real engine uses a radix tree, but what matters for measuring routing
// önemli olan davranış "hangi blokların hâlâ cache'te olduğu"dur ve onu LRU // quality is which blocks are still resident, and LRU models that faithfully.
// doğru modeller.
type PrefixCache struct { type PrefixCache struct {
capacity int // blok cinsinden capacity int // in blocks
ll *list.List ll *list.List
m map[BlockHash]*list.Element m map[BlockHash]*list.Element
@@ -53,8 +52,8 @@ type PrefixCache struct {
hits int64 hits int64
} }
// NewPrefixCache, verilen blok kapasitesinde bir cache kurar. // NewPrefixCache creates a cache holding at most capacity blocks.
// capacity <= 0 ise cache devre dışıdır (her zaman miss). // A capacity of zero or less disables the cache (every lookup misses).
func NewPrefixCache(capacity int) *PrefixCache { func NewPrefixCache(capacity int) *PrefixCache {
return &PrefixCache{ return &PrefixCache{
capacity: capacity, capacity: capacity,
@@ -63,14 +62,14 @@ func NewPrefixCache(capacity int) *PrefixCache {
} }
} }
// Enabled, cache'in açık olup olmadığını söyler. // Enabled reports whether the cache is active.
func (c *PrefixCache) Enabled() bool { return c != nil && c.capacity > 0 } func (c *PrefixCache) Enabled() bool { return c != nil && c.capacity > 0 }
// Lookup, hashes dizisinin cache'te bulunan **kesintisiz ön ek** uzunluğunu // Lookup returns the length, in blocks, of the longest **contiguous prefix**
// blok cinsinden döner ve bulunan blokları LRU'da tazeler. // of hashes present in the cache, refreshing those blocks in the LRU.
// //
// Kesintisiz olması önemli: ortadaki bir blok düşmüşse ondan sonrası da // Contiguity matters: if a block in the middle was evicted, everything after
// yeniden hesaplanmak zorundadır, çünkü attention önceki tüm bloklara bağlıdır. // it must be recomputed too, because attention depends on all preceding blocks.
func (c *PrefixCache) Lookup(hashes []BlockHash) int { func (c *PrefixCache) Lookup(hashes []BlockHash) int {
if !c.Enabled() { if !c.Enabled() {
return 0 return 0
@@ -89,7 +88,8 @@ func (c *PrefixCache) Lookup(hashes []BlockHash) int {
return n return n
} }
// Insert, blokları cache'e yazar (LRU'nun başına) ve kapasiteyi aşarsa en eskileri atar. // Insert stores blocks at the front of the LRU, evicting the oldest ones once
// capacity is exceeded.
func (c *PrefixCache) Insert(hashes []BlockHash) { func (c *PrefixCache) Insert(hashes []BlockHash) {
if !c.Enabled() { if !c.Enabled() {
return return
@@ -111,7 +111,7 @@ func (c *PrefixCache) Insert(hashes []BlockHash) {
} }
} }
// Stats, cache sorgu ve isabet sayılarını döner (blok cinsinden). // Stats returns cumulative query and hit counts, in blocks.
func (c *PrefixCache) Stats() (queries, hits int64) { func (c *PrefixCache) Stats() (queries, hits int64) {
if c == nil { if c == nil {
return 0, 0 return 0, 0
@@ -119,7 +119,7 @@ func (c *PrefixCache) Stats() (queries, hits int64) {
return c.queries, c.hits return c.queries, c.hits
} }
// Len, cache'teki blok sayısı. // Len returns the number of blocks currently cached.
func (c *PrefixCache) Len() int { func (c *PrefixCache) Len() int {
if !c.Enabled() { if !c.Enabled() {
return 0 return 0
+27 -27
View File
@@ -2,76 +2,76 @@ package sim
import "testing" import "testing"
func TestBlockHashesZincirleme(t *testing.T) { func TestBlockHashesAreChained(t *testing.T) {
// Aynı içerik farklı ön ekten sonra geliyorsa blok hash'i farklı olmalı. // The same content following a different prefix must hash differently.
a := BlockHashes(Tokenize("AAAAAAAABBBBBBBB"), 2) a := BlockHashes(Tokenize("AAAAAAAABBBBBBBB"), 2)
b := BlockHashes(Tokenize("CCCCCCCCBBBBBBBB"), 2) b := BlockHashes(Tokenize("CCCCCCCCBBBBBBBB"), 2)
if len(a) != 2 || len(b) != 2 { if len(a) != 2 || len(b) != 2 {
t.Fatalf("beklenen 2 blok, alınan a=%d b=%d", len(a), len(b)) t.Fatalf("expected 2 blocks each, got a=%d b=%d", len(a), len(b))
} }
if a[1] == b[1] { if a[1] == b[1] {
t.Error("farklı ön ekten sonra gelen aynı blok aynı hash'i verdi; zincirleme çalışmıyor") t.Error("identical block after different prefixes hashed the same; chaining is broken")
} }
} }
func TestBlockHashesOnEkKararli(t *testing.T) { func TestBlockHashesArePrefixStable(t *testing.T) {
// Blok sınırına hizalı bir ön ek, uzun metinde aynı blok hash'lerini vermeli. // A prefix aligned to a block boundary must hash identically in a longer
// Hizalama şart: son yarım token farklı olursa onu içeren blok da farklı // text. Alignment matters: a differing trailing partial token changes the
// çıkar — gerçek tokenizer'larda da olan sınır etkisi, bu yüzden prefix // block containing it — the same boundary effect real tokenizers have,
// cache hit'i her zaman tam blok cinsinden ölçülür. // which is why cache hits are always measured in whole blocks.
const aligned = "merhaba " // 8 bayt = 2 token = blockSize 2 ile 1 tam blok const aligned = "merhaba " // 8 bytes = 2 tokens = 1 full block at blockSize 2
short := BlockHashes(Tokenize(aligned), 2) short := BlockHashes(Tokenize(aligned), 2)
long := BlockHashes(Tokenize(aligned+"dunya nasilsin"), 2) long := BlockHashes(Tokenize(aligned+"dunya nasilsin"), 2)
if len(short) != 1 { if len(short) != 1 {
t.Fatalf("beklenen 1 blok, alınan %d", len(short)) t.Fatalf("expected 1 block, got %d", len(short))
} }
if short[0] != long[0] { if short[0] != long[0] {
t.Fatal("hizalı ön ek blok hash'i değişti; ön ek kararlı değil") t.Fatal("aligned prefix produced a different block hash; not prefix stable")
} }
} }
func TestBlockHashesYarimBlokAtilir(t *testing.T) { func TestBlockHashesDropPartialBlock(t *testing.T) {
// 3 token, blockSize 2 -> 1 tam blok // 3 tokens at blockSize 2 -> 1 full block
if got := len(BlockHashes(make([]Token, 3), 2)); got != 1 { if got := len(BlockHashes(make([]Token, 3), 2)); got != 1 {
t.Errorf("beklenen 1 blok, alınan %d", got) t.Errorf("expected 1 block, got %d", got)
} }
if got := len(BlockHashes(make([]Token, 1), 2)); got != 0 { if got := len(BlockHashes(make([]Token, 1), 2)); got != 0 {
t.Errorf("tam blok yokken blok üretildi: %d", got) t.Errorf("produced %d blocks with no full block available", got)
} }
} }
func TestPrefixCacheKesintisizOnEk(t *testing.T) { func TestPrefixCacheReturnsContiguousPrefix(t *testing.T) {
c := NewPrefixCache(10) c := NewPrefixCache(10)
h := []BlockHash{1, 2, 3, 4} h := []BlockHash{1, 2, 3, 4}
c.Insert(h) c.Insert(h)
if n := c.Lookup(h); n != 4 { if n := c.Lookup(h); n != 4 {
t.Fatalf("tam eşleşme beklendi, alınan %d", n) t.Fatalf("expected a full match, got %d", n)
} }
// Ortası eksik: ilk blok hit, sonrası miss olmalı. // With a gap in the middle only the first block counts.
if n := c.Lookup([]BlockHash{1, 99, 3}); n != 1 { if n := c.Lookup([]BlockHash{1, 99, 3}); n != 1 {
t.Errorf("kesintisiz ön ek 1 olmalıydı, alınan %d", n) t.Errorf("contiguous prefix should have been 1, got %d", n)
} }
} }
func TestPrefixCacheLRUTahliye(t *testing.T) { func TestPrefixCacheEvictsLRU(t *testing.T) {
c := NewPrefixCache(2) c := NewPrefixCache(2)
c.Insert([]BlockHash{1, 2}) c.Insert([]BlockHash{1, 2})
c.Lookup([]BlockHash{1}) // 1'i tazele c.Lookup([]BlockHash{1}) // refresh 1
c.Insert([]BlockHash{3}) // kapasite aşıldı, en eski (2) düşmeli c.Insert([]BlockHash{3}) // over capacity: the oldest (2) must go
if n := c.Lookup([]BlockHash{1}); n != 1 { if n := c.Lookup([]BlockHash{1}); n != 1 {
t.Error("taze blok tahliye edildi") t.Error("recently used block was evicted")
} }
if n := c.Lookup([]BlockHash{2}); n != 0 { if n := c.Lookup([]BlockHash{2}); n != 0 {
t.Error("en eski blok tahliye edilmeliydi") t.Error("least recently used block should have been evicted")
} }
} }
func TestPrefixCacheKapali(t *testing.T) { func TestPrefixCacheDisabled(t *testing.T) {
c := NewPrefixCache(0) c := NewPrefixCache(0)
c.Insert([]BlockHash{1, 2}) c.Insert([]BlockHash{1, 2})
if n := c.Lookup([]BlockHash{1}); n != 0 { if n := c.Lookup([]BlockHash{1}); n != 0 {
t.Errorf("kapalı cache hit verdi: %d", n) t.Errorf("disabled cache reported a hit: %d", n)
} }
} }
+69 -70
View File
@@ -1,12 +1,12 @@
// Package sim, OpenAI uyumlu sahte bir inference engine'i barındırır. // Package sim provides an OpenAI-compatible fake inference engine.
// //
// Amacı router'ı GPU olmadan gerçekçi koşullarda test edebilmek. Gerçek bir // Its purpose is to let the router be tested under realistic conditions
// engine'in router açısından önemli olan davranışlarını modeller: // without GPUs. It models the real-engine behaviours the router cares about:
// - continuous batching (sınırlı eşzamanlı dizi sayısı) // - continuous batching (bounded number of concurrent sequences)
// - KV cache blok muhasebesi ve blok yetmediğinde kuyruğa alma // - KV cache block accounting, queueing once blocks run out
// - otomatik prefix cache (blok bazlı LRU) ve cache hit'in TTFT'ye etkisi // - automatic prefix caching (block-level LRU) and its effect on TTFT
// - batch büyüdükçe token başına gecikmenin artması // - per-token latency growing as the batch grows
// - vLLM formatında /metrics // - /metrics in vLLM's format
package sim package sim
import ( import (
@@ -19,40 +19,40 @@ import (
"time" "time"
) )
// Config, simüle edilen engine'in davranışını belirler. // Config controls the simulated engine's behaviour.
type Config struct { type Config struct {
Model string Model string
// MaxNumSeqs, aynı anda çalışabilecek dizi sayısı (continuous batch kapasitesi). // MaxNumSeqs is how many sequences may run concurrently (batch capacity).
MaxNumSeqs int MaxNumSeqs int
// BlockSize, bir KV cache bloğundaki token sayısı. // BlockSize is the number of tokens per KV cache block.
BlockSize int BlockSize int
// NumGPUBlocks, toplam KV cache bloğu. // NumGPUBlocks is the total number of KV cache blocks.
NumGPUBlocks int NumGPUBlocks int
// PrefixCacheBlocks, prefix cache'in tutacağı blok sayısı. 0 = kapalı. // PrefixCacheBlocks is the prefix cache capacity in blocks. 0 disables it.
PrefixCacheBlocks int PrefixCacheBlocks int
// PrefillTokensPerSec, engine genelinde paylaşılan prefill hızı. // PrefillTokensPerSec is the prefill rate, shared engine-wide.
PrefillTokensPerSec float64 PrefillTokensPerSec float64
// DecodeTokensPerSec, tek dizi çalışırken token üretim hızı. // DecodeTokensPerSec is the generation rate for a single running sequence.
DecodeTokensPerSec float64 DecodeTokensPerSec float64
// BatchContention, batch'teki her ek dizinin decode hızını ne kadar // BatchContention is how much each additional sequence slows decoding:
// yavaşlattığı. Etkin hız = DecodeTokensPerSec / (1 + BatchContention*(n-1)). // effective rate = DecodeTokensPerSec / (1 + BatchContention*(n-1)).
BatchContention float64 BatchContention float64
// DefaultOutputTokens, istek max_tokens vermediğinde üretilecek token sayısı. // DefaultOutputTokens is generated when a request omits max_tokens.
DefaultOutputTokens int DefaultOutputTokens int
// Tick, simülasyon adımı. Küçük olması çözünürlüğü artırır. // Tick is the simulation step; smaller values increase resolution.
Tick time.Duration Tick time.Duration
// FailRate, isteklerin hangi oranda 500 ile reddedileceği (kaos testi için). // FailRate is the fraction of requests rejected artificially, for chaos tests.
FailRate float64 FailRate float64
// Seed, deterministik testler için. // Seed makes runs deterministic.
Seed int64 Seed int64
} }
// Default, makul varsayılanlarla bir Config döner. // Default returns a configuration with sensible values, roughly in the range
// Kabaca 8xH100 üstünde 70B bir modelin davranışına benzer ölçekler. // of a 70B model on 8xH100.
func Default() Config { func Default() Config {
return Config{ return Config{
Model: "sim-model", Model: "sim-model",
@@ -95,34 +95,34 @@ func (c *Config) normalize() {
} }
} }
// EventKind, akıştaki olay türü. // EventKind is the type of an event in the stream.
type EventKind int type EventKind int
const ( const (
// EventToken, üretilen bir token. // EventToken carries one generated token.
EventToken EventKind = iota EventToken EventKind = iota
// EventDone, üretimin normal bitişi. // EventDone marks normal completion.
EventDone EventDone
// EventError, üretim sırasında oluşan hata. // EventError reports a failure during generation.
EventError EventError
) )
// Event, engine'in istemciye ilettiği tek bir olay. // Event is a single event delivered to the caller.
type Event struct { type Event struct {
Kind EventKind Kind EventKind
Text string Text string
Err error Err error
// Result yalnızca EventDone'da doludur; prompt/cache/completion sayılarını taşır. // Result is set only on EventDone; it carries the final token counts.
Result *Result Result *Result
} }
// Prompt, engine'e verilen istek. // Prompt is a request handed to the engine.
type Prompt struct { type Prompt struct {
Text string Text string
MaxTokens int MaxTokens int
} }
// Result, istek bittiğinde okunabilen özet (Done olayından sonra geçerli). // Result summarizes a finished request; valid from the Done event onwards.
type Result struct { type Result struct {
PromptTokens int PromptTokens int
CachedTokens int CachedTokens int
@@ -130,12 +130,12 @@ type Result struct {
} }
var ( var (
// ErrTooLong, isteğin KV cache'e hiç sığmadığını belirtir. // ErrTooLong means the request does not fit in the KV cache at all.
ErrTooLong = errors.New("istek KV cache kapasitesine sığmıyor") ErrTooLong = errors.New("request does not fit in KV cache capacity")
// ErrInjected, FailRate ile üretilen yapay hata. // ErrInjected is the artificial failure produced by FailRate.
ErrInjected = errors.New("yapay hata (sim fail_rate)") ErrInjected = errors.New("injected failure (sim fail_rate)")
// ErrShutdown, engine kapanırken bekleyen istekler için. // ErrShutdown is returned to pending requests while the engine stops.
ErrShutdown = errors.New("engine kapanıyor") ErrShutdown = errors.New("engine shutting down")
) )
type req struct { type req struct {
@@ -164,9 +164,9 @@ type req struct {
res *Result res *Result
} }
// flush, biriken olayları bloke olmadan kanala aktarır. // flush moves buffered events into the channel without blocking.
// Bloke olmamak şart: tick döngüsü kilidi tutarken yavaş bir istemci // Not blocking is essential: the tick loop holds the engine lock, and one slow
// tüm engine'i durdurmamalı. // client must not stall every other request.
func (r *req) flush() { func (r *req) flush() {
for len(r.pending) > 0 { for len(r.pending) > 0 {
select { select {
@@ -183,7 +183,7 @@ func (r *req) emit(ev Event) {
r.flush() r.flush()
} }
// Engine, simüle edilen inference engine. // Engine is the simulated inference engine.
type Engine struct { type Engine struct {
cfg Config cfg Config
@@ -203,8 +203,7 @@ type Engine struct {
done chan struct{} done chan struct{}
} }
// New, verilen konfigürasyonla bir engine kurar ve simülasyon döngüsünü başlatır. // New creates an engine and starts its simulation loop. Call Close to stop it.
// Kapatmak için Close çağrılmalı.
func New(cfg Config) *Engine { func New(cfg Config) *Engine {
cfg.normalize() cfg.normalize()
seed := cfg.Seed seed := cfg.Seed
@@ -215,7 +214,7 @@ func New(cfg Config) *Engine {
cfg: cfg, cfg: cfg,
freeBlocks: cfg.NumGPUBlocks, freeBlocks: cfg.NumGPUBlocks,
cache: NewPrefixCache(cfg.PrefixCacheBlocks), cache: NewPrefixCache(cfg.PrefixCacheBlocks),
rng: rand.New(rand.NewSource(seed)), //nolint:gosec // simülasyon rng: rand.New(rand.NewSource(seed)), //nolint:gosec // simulation only
m: newMetrics(), m: newMetrics(),
stop: make(chan struct{}), stop: make(chan struct{}),
done: make(chan struct{}), done: make(chan struct{}),
@@ -224,10 +223,10 @@ func New(cfg Config) *Engine {
return e return e
} }
// Config, engine'in aktif konfigürasyonunu döner. // Config returns the engine's effective configuration.
func (e *Engine) Config() Config { return e.cfg } func (e *Engine) Config() Config { return e.cfg }
// Close, simülasyon döngüsünü durdurur ve bekleyen istekleri hataya düşürür. // Close stops the simulation loop and fails any pending requests.
func (e *Engine) Close() { func (e *Engine) Close() {
e.stopOnce.Do(func() { e.stopOnce.Do(func() {
close(e.stop) close(e.stop)
@@ -235,8 +234,8 @@ func (e *Engine) Close() {
}) })
} }
// Submit, isteği kuyruğa alır ve olay kanalını döner. // Submit enqueues a request and returns its event channel. The channel closes
// Kanal, üretim bitince veya hata oluşunca kapanır. // when generation finishes or fails.
func (e *Engine) Submit(ctx context.Context, p Prompt) (<-chan Event, error) { func (e *Engine) Submit(ctx context.Context, p Prompt) (<-chan Event, error) {
if e.cfg.FailRate > 0 { if e.cfg.FailRate > 0 {
e.mu.Lock() e.mu.Lock()
@@ -310,9 +309,9 @@ func (e *Engine) run() {
} }
} }
// reap, istemcisi vazgeçmiş istekleri hem kuyruktan hem batch'ten düşürür. // reap drops requests whose client has gone away, from both the queue and the
// Router'ın iptal propagasyonunu test edebilmek için bu davranış şart: // running batch. The router's cancellation propagation cannot be tested
// istemci koptuğunda GPU boşa çalışmamalı. // without this: when a client disconnects, the GPU must stop working.
func (e *Engine) reap() { func (e *Engine) reap() {
e.waiting = filterInPlace(e.waiting, func(r *req) bool { e.waiting = filterInPlace(e.waiting, func(r *req) bool {
if r.ctx.Err() == nil { if r.ctx.Err() == nil {
@@ -338,7 +337,7 @@ func (e *Engine) schedule() {
r := e.waiting[0] r := e.waiting[0]
need := e.blocksFor(r.promptTokens + r.outputTokens) need := e.blocksFor(r.promptTokens + r.outputTokens)
if need > e.freeBlocks { if need > e.freeBlocks {
// Head-of-line blocking: gerçek engine de böyle davranır. // Head-of-line blocking; real engines behave the same way.
break break
} }
@@ -362,7 +361,7 @@ func (e *Engine) advance(dt float64) {
return return
} }
// Prefill bütçesi engine genelinde paylaşılır ve FIFO dağıtılır. // The prefill budget is shared engine-wide and handed out FIFO.
budget := e.cfg.PrefillTokensPerSec * dt budget := e.cfg.PrefillTokensPerSec * dt
for _, r := range e.running { for _, r := range e.running {
if budget <= 0 { if budget <= 0 {
@@ -376,7 +375,7 @@ func (e *Engine) advance(dt float64) {
budget -= used budget -= used
} }
// Decode hızı batch büyüdükçe düşer. // Decoding slows down as the batch grows.
n := 0 n := 0
for _, r := range e.running { for _, r := range e.running {
if r.prefillLeft <= 0 { if r.prefillLeft <= 0 {
@@ -420,9 +419,9 @@ func (e *Engine) finish(r *req) {
r.finished = true r.finished = true
e.freeBlocks += r.blocks e.freeBlocks += r.blocks
// Üretilen çıktı da cache'e girer: bir sonraki turda konuşma geçmişi // The generated output enters the cache too: when it comes back as
// olarak geri geldiğinde hit vermeli. Çok turlu sohbeti modellemenin // conversation history on the next turn it must hit. That is the only way
// tek yolu bu. // to model multi-turn chat.
full := Tokenize(r.prompt.Text + generatedText(r.emitted)) full := Tokenize(r.prompt.Text + generatedText(r.emitted))
e.cache.Insert(BlockHashes(full, e.cfg.BlockSize)) e.cache.Insert(BlockHashes(full, e.cfg.BlockSize))
@@ -469,7 +468,19 @@ func (e *Engine) shutdown() {
e.waiting, e.running, e.draining = nil, nil, nil e.waiting, e.running, e.draining = nil, nil, nil
} }
// Stats, router'ın scrape edeceği anlık durumu döner. // Stats returns the instantaneous state the router would scrape.
type Stats struct {
Running int
Waiting int
KVCacheUsage float64
BlockSize int
NumGPUBlocks int
PrefixQueries int64
PrefixHits int64
CachedBlocks int
}
// Stats returns a snapshot of the engine's current state.
func (e *Engine) Stats() Stats { func (e *Engine) Stats() Stats {
e.mu.Lock() e.mu.Lock()
defer e.mu.Unlock() defer e.mu.Unlock()
@@ -487,18 +498,6 @@ func (e *Engine) Stats() Stats {
} }
} }
// Stats, engine'in anlık durumu.
type Stats struct {
Running int
Waiting int
KVCacheUsage float64
BlockSize int
NumGPUBlocks int
PrefixQueries int64
PrefixHits int64
CachedBlocks int
}
func filterInPlace(s []*req, keep func(*req) bool) []*req { func filterInPlace(s []*req, keep func(*req) bool) []*req {
out := s[:0] out := s[:0]
for _, r := range s { for _, r := range s {
+39 -42
View File
@@ -7,7 +7,7 @@ import (
"time" "time"
) )
// testConfig, testlerin hızlı ve deterministik koşması için hızlandırılmış ayarlar. // testConfig speeds the simulation up so tests stay fast and deterministic.
func testConfig() Config { func testConfig() Config {
c := Default() c := Default()
c.Tick = time.Millisecond c.Tick = time.Millisecond
@@ -18,7 +18,7 @@ func testConfig() Config {
return c return c
} }
// drain, kanaldaki tüm token'ları toplar ve bitişi bekler. // drain collects every token from the channel and waits for completion.
func drain(t *testing.T, ch <-chan Event) (text string, done bool) { func drain(t *testing.T, ch <-chan Event) (text string, done bool) {
t.Helper() t.Helper()
var b strings.Builder var b strings.Builder
@@ -35,36 +35,36 @@ func drain(t *testing.T, ch <-chan Event) (text string, done bool) {
case EventDone: case EventDone:
done = true done = true
case EventError: case EventError:
t.Fatalf("beklenmeyen hata: %v", ev.Err) t.Fatalf("unexpected error: %v", ev.Err)
} }
case <-deadline: case <-deadline:
t.Fatal("zaman aşımı: engine token üretmedi") t.Fatal("timed out waiting for tokens")
} }
} }
} }
func TestEngineTemelUretim(t *testing.T) { func TestEngineGeneratesTokens(t *testing.T) {
e := New(testConfig()) e := New(testConfig())
defer e.Close() defer e.Close()
ch, err := e.Submit(t.Context(), Prompt{Text: "merhaba dunya", MaxTokens: 10}) ch, err := e.Submit(t.Context(), Prompt{Text: "hello world", MaxTokens: 10})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
text, done := drain(t, ch) text, done := drain(t, ch)
if !done { if !done {
t.Error("EventDone gelmedi") t.Error("no EventDone received")
} }
if got := len(strings.Fields(text)); got != 10 { if got := len(strings.Fields(text)); got != 10 {
t.Errorf("beklenen 10 token, alınan %d (%q)", got, text) t.Errorf("expected 10 tokens, got %d (%q)", got, text)
} }
} }
func TestEngineIptalBloklariSerbestBirakir(t *testing.T) { func TestCancellationReleasesBlocks(t *testing.T) {
// Router'ın iptal propagasyonunu test edebilmesi için kritik davranış: // Essential for testing the router's cancellation propagation: when the
// istemci koptuğunda engine isteği düşürmeli ve KV bloklarını bırakmalı. // client goes away the engine must drop the request and free its KV blocks.
cfg := testConfig() cfg := testConfig()
cfg.DecodeTokensPerSec = 20 // uzun sürsün ki iptal ederken çalışıyor olsun cfg.DecodeTokensPerSec = 20 // slow, so it is still running when cancelled
e := New(cfg) e := New(cfg)
defer e.Close() defer e.Close()
@@ -76,7 +76,7 @@ func TestEngineIptalBloklariSerbestBirakir(t *testing.T) {
waitFor(t, time.Second, func() bool { return e.Stats().Running == 1 }) waitFor(t, time.Second, func() bool { return e.Stats().Running == 1 })
if usage := e.Stats().KVCacheUsage; usage <= 0 { if usage := e.Stats().KVCacheUsage; usage <= 0 {
t.Fatal("çalışan istek KV bloğu ayırmadı") t.Fatal("running request reserved no KV blocks")
} }
cancel() cancel()
@@ -86,20 +86,19 @@ func TestEngineIptalBloklariSerbestBirakir(t *testing.T) {
return s.Running == 0 && s.KVCacheUsage == 0 return s.Running == 0 && s.KVCacheUsage == 0
}) })
// Kanal kapanmalı ki istemci tarafı bloke kalmasın. // The channel must close so the client side never blocks.
select { select {
case _, open := <-ch: case _, open := <-ch:
if open { if open {
// token kalmış olabilir; kapanana kadar tüket for range ch { //nolint:revive // drain remaining events
for range ch { //nolint:revive // kanalı boşalt
} }
} }
case <-time.After(2 * time.Second): case <-time.After(2 * time.Second):
t.Fatal("iptal sonrası kanal kapanmadı") t.Fatal("channel stayed open after cancellation")
} }
} }
func TestEngineBatchDolunkaKuyrugaAlir(t *testing.T) { func TestRequestsQueueWhenBatchIsFull(t *testing.T) {
cfg := testConfig() cfg := testConfig()
cfg.MaxNumSeqs = 2 cfg.MaxNumSeqs = 2
cfg.DecodeTokensPerSec = 20 cfg.DecodeTokensPerSec = 20
@@ -115,56 +114,54 @@ func TestEngineBatchDolunkaKuyrugaAlir(t *testing.T) {
waitFor(t, time.Second, func() bool { return e.Stats().Running == 2 }) waitFor(t, time.Second, func() bool { return e.Stats().Running == 2 })
s := e.Stats() s := e.Stats()
if s.Running != 2 { if s.Running != 2 {
t.Errorf("running %d, MaxNumSeqs=2 aşıldı", s.Running) t.Errorf("running=%d exceeds MaxNumSeqs=2", s.Running)
} }
if s.Waiting != 3 { if s.Waiting != 3 {
t.Errorf("waiting %d, beklenen 3", s.Waiting) t.Errorf("waiting=%d, expected 3", s.Waiting)
} }
} }
func TestEngineKVBlogunaSigmayanIstekReddedilir(t *testing.T) { func TestOversizedRequestIsRejected(t *testing.T) {
cfg := testConfig() cfg := testConfig()
cfg.NumGPUBlocks = 4 cfg.NumGPUBlocks = 4
cfg.BlockSize = 16 // toplam 64 token kapasite cfg.BlockSize = 16 // 64 tokens of capacity in total
e := New(cfg) e := New(cfg)
defer e.Close() defer e.Close()
_, err := e.Submit(t.Context(), Prompt{Text: strings.Repeat("x", 10000), MaxTokens: 100}) _, err := e.Submit(t.Context(), Prompt{Text: strings.Repeat("x", 10000), MaxTokens: 100})
if err != ErrTooLong { if err != ErrTooLong {
t.Errorf("ErrTooLong beklendi, alınan %v", err) t.Errorf("expected ErrTooLong, got %v", err)
} }
} }
func TestEnginePrefixCacheIkinciIstegiHizlandirir(t *testing.T) { func TestPrefixCacheSpeedsUpRepeatedPrompt(t *testing.T) {
cfg := testConfig() cfg := testConfig()
cfg.PrefillTokensPerSec = 2000 // prefill maliyeti ölçülebilir olsun cfg.PrefillTokensPerSec = 2000 // make prefill cost measurable
cfg.DecodeTokensPerSec = 5000 cfg.DecodeTokensPerSec = 5000
e := New(cfg) e := New(cfg)
defer e.Close() defer e.Close()
prompt := Prompt{Text: strings.Repeat("ortak sistem promptu. ", 200), MaxTokens: 5} prompt := Prompt{Text: strings.Repeat("shared system prompt. ", 200), MaxTokens: 5}
first := timeToFirstToken(t, e, prompt) first := timeToFirstToken(t, e, prompt)
// İlk istek bitip cache'e yazıldıktan sonra aynısını tekrar gönder. // Send the same prompt again now that the first one populated the cache.
second := timeToFirstToken(t, e, prompt) second := timeToFirstToken(t, e, prompt)
if second >= first { if second >= first {
t.Errorf("prefix cache TTFT'yi düşürmedi: ilk=%v ikinci=%v", first, second) t.Errorf("prefix cache did not reduce TTFT: first=%v second=%v", first, second)
} }
if e.Stats().PrefixHits == 0 {
s := e.Stats() t.Error("prefix cache hit counter did not advance")
if s.PrefixHits == 0 {
t.Error("prefix cache hit sayacı artmadı")
} }
} }
func TestEnginePrefixCacheKapaliykenHitYok(t *testing.T) { func TestNoHitsWhenPrefixCacheDisabled(t *testing.T) {
cfg := testConfig() cfg := testConfig()
cfg.PrefixCacheBlocks = 0 cfg.PrefixCacheBlocks = 0
e := New(cfg) e := New(cfg)
defer e.Close() defer e.Close()
p := Prompt{Text: strings.Repeat("ayni prompt. ", 50), MaxTokens: 3} p := Prompt{Text: strings.Repeat("same prompt. ", 50), MaxTokens: 3}
for range 3 { for range 3 {
ch, err := e.Submit(t.Context(), p) ch, err := e.Submit(t.Context(), p)
if err != nil { if err != nil {
@@ -173,11 +170,11 @@ func TestEnginePrefixCacheKapaliykenHitYok(t *testing.T) {
drain(t, ch) drain(t, ch)
} }
if h := e.Stats().PrefixHits; h != 0 { if h := e.Stats().PrefixHits; h != 0 {
t.Errorf("cache kapalıyken %d hit raporlandı", h) t.Errorf("disabled cache reported %d hits", h)
} }
} }
func TestEngineKapanisBekleyenleriBosaltir(t *testing.T) { func TestShutdownDrainsPendingRequests(t *testing.T) {
cfg := testConfig() cfg := testConfig()
cfg.DecodeTokensPerSec = 1 cfg.DecodeTokensPerSec = 1
e := New(cfg) e := New(cfg)
@@ -197,23 +194,23 @@ func TestEngineKapanisBekleyenleriBosaltir(t *testing.T) {
return return
} }
case <-deadline: case <-deadline:
t.Fatal("kapanışta kanal kapanmadı") t.Fatal("channel stayed open after shutdown")
} }
} }
} }
func TestEngineYapayHata(t *testing.T) { func TestInjectedFailure(t *testing.T) {
cfg := testConfig() cfg := testConfig()
cfg.FailRate = 1.0 cfg.FailRate = 1.0
e := New(cfg) e := New(cfg)
defer e.Close() defer e.Close()
if _, err := e.Submit(t.Context(), Prompt{Text: "abc"}); err != ErrInjected { if _, err := e.Submit(t.Context(), Prompt{Text: "abc"}); err != ErrInjected {
t.Errorf("ErrInjected beklendi, alınan %v", err) t.Errorf("expected ErrInjected, got %v", err)
} }
} }
// --- yardımcılar --- // --- helpers ---
func timeToFirstToken(t *testing.T, e *Engine, p Prompt) time.Duration { func timeToFirstToken(t *testing.T, e *Engine, p Prompt) time.Duration {
t.Helper() t.Helper()
@@ -229,7 +226,7 @@ func timeToFirstToken(t *testing.T, e *Engine, p Prompt) time.Duration {
} }
} }
if ttft == 0 { if ttft == 0 {
t.Fatal("hiç token üretilmedi") t.Fatal("no tokens were generated")
} }
return ttft return ttft
} }
@@ -243,5 +240,5 @@ func waitFor(t *testing.T, limit time.Duration, cond func() bool) {
} }
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
} }
t.Fatalf("koşul %v içinde sağlanmadı", limit) t.Fatalf("condition not met within %v", limit)
} }
+9 -9
View File
@@ -8,13 +8,13 @@ import (
"time" "time"
) )
// ttftBuckets ve tpotBuckets, vLLM'in kullandıklarına yakın saniye sınırları. // ttftBuckets and tpotBuckets use second boundaries close to vLLM's own.
var ( var (
ttftBuckets = []float64{0.001, 0.005, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, 20, 40, 80} ttftBuckets = []float64{0.001, 0.005, 0.01, 0.02, 0.04, 0.06, 0.08, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, 20, 40, 80}
tpotBuckets = []float64{0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 2.5} tpotBuckets = []float64{0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.75, 1, 2.5}
) )
// histogram, Prometheus uyumlu basit bir kümülatif histogram. // histogram is a minimal Prometheus-compatible cumulative histogram.
type histogram struct { type histogram struct {
bounds []float64 bounds []float64
counts []uint64 counts []uint64
@@ -36,7 +36,7 @@ func (h *histogram) observe(v float64) {
} }
} }
// write, histogramı Prometheus text formatında yazar (kümülatif le bucket'ları). // write renders the histogram in Prometheus text format (cumulative le buckets).
func (h *histogram) write(w io.Writer, name, labels string) { func (h *histogram) write(w io.Writer, name, labels string) {
var cum uint64 var cum uint64
for i, b := range h.bounds { for i, b := range h.bounds {
@@ -81,12 +81,12 @@ func (m *metrics) observeTPOT(d time.Duration) {
m.mu.Unlock() m.mu.Unlock()
} }
// WriteMetrics, engine durumunu vLLM'in metrik adlarıyla Prometheus text // WriteMetrics renders the engine's state in Prometheus text format using
// formatında yazar. // vLLM's metric names.
// //
// Metrik adları §7.6'daki engine kontratıdır; router bunlara göre parse eder. // Those names are the engine contract from plan §7.6; the router parses
// Gerçek vLLM sürümleriyle aynı adları kullanmak şart — sim-engine'in varlık // against them. Matching real vLLM names exactly is the point: sim-engine
// sebebi router'ın gerçek engine'e karşı davranışını taklit etmek. // exists to stand in for a real engine as far as the router can tell.
func (e *Engine) WriteMetrics(w io.Writer) { func (e *Engine) WriteMetrics(w io.Writer) {
s := e.Stats() s := e.Stats()
lbl := fmt.Sprintf("model_name=%q", e.cfg.Model) lbl := fmt.Sprintf("model_name=%q", e.cfg.Model)
@@ -127,7 +127,7 @@ func (e *Engine) WriteMetrics(w io.Writer) {
fmt.Fprintf(w, "# TYPE vllm:request_success_total counter\n") fmt.Fprintf(w, "# TYPE vllm:request_success_total counter\n")
fmt.Fprintf(w, "vllm:request_success_total{%s,finished_reason=\"length\"} %d\n", lbl, e.m.finished.Load()) fmt.Fprintf(w, "vllm:request_success_total{%s,finished_reason=\"length\"} %d\n", lbl, e.m.finished.Load())
// Simülasyona özgü (gerçek vLLM'de yok; kaos testlerinde işe yarar). // Simulation-specific (absent in real vLLM; useful for chaos tests).
fmt.Fprintf(w, "# TYPE sim:requests_cancelled_total counter\n") fmt.Fprintf(w, "# TYPE sim:requests_cancelled_total counter\n")
fmt.Fprintf(w, "sim:requests_cancelled_total{%s} %d\n", lbl, e.m.cancelled.Load()) fmt.Fprintf(w, "sim:requests_cancelled_total{%s} %d\n", lbl, e.m.cancelled.Load())
fmt.Fprintf(w, "# TYPE sim:requests_failed_total counter\n") fmt.Fprintf(w, "# TYPE sim:requests_failed_total counter\n")
+16 -16
View File
@@ -9,14 +9,14 @@ import (
"time" "time"
) )
// Server, Engine'i OpenAI uyumlu bir HTTP yüzeyiyle sarar. // Server wraps an Engine in an OpenAI-compatible HTTP surface.
type Server struct { type Server struct {
eng *Engine eng *Engine
log *slog.Logger log *slog.Logger
mux *http.ServeMux mux *http.ServeMux
} }
// NewServer, engine için HTTP yüzeyini kurar. // NewServer builds the HTTP surface for an engine.
func NewServer(eng *Engine, log *slog.Logger) *Server { func NewServer(eng *Engine, log *slog.Logger) *Server {
if log == nil { if log == nil {
log = slog.Default() log = slog.Default()
@@ -32,7 +32,7 @@ func NewServer(eng *Engine, log *slog.Logger) *Server {
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) } func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.mux.ServeHTTP(w, r) }
// --- istek/yanıt tipleri (yalnızca gereken alanlar) --- // --- request/response types (only the fields we need) ---
type streamOptions struct { type streamOptions struct {
IncludeUsage bool `json:"include_usage"` IncludeUsage bool `json:"include_usage"`
@@ -64,15 +64,15 @@ type usage struct {
PromptTokens int `json:"prompt_tokens"` PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"` CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"` TotalTokens int `json:"total_tokens"`
// OpenAI uyumlu cache raporlaması; prefix cache etkisini istemciye gösterir. // OpenAI-compatible cache reporting; surfaces the prefix cache to clients.
PromptTokensDetails struct { PromptTokensDetails struct {
CachedTokens int `json:"cached_tokens"` CachedTokens int `json:"cached_tokens"`
} `json:"prompt_tokens_details"` } `json:"prompt_tokens_details"`
} }
// text, mesaj içeriğini düz metne indirger. // text reduces a message's content to plain text.
// İçerik string ya da parça dizisi olabilir; sim için ikisini de kaba şekilde // Content may be a string or an array of parts; the simulation coerces both
// metne çeviriyoruz, çünkü tek ihtiyacımız deterministik bir token sayısı. // crudely, since all it needs is a deterministic token count.
func (m chatMessage) text() string { func (m chatMessage) text() string {
var s string var s string
if err := json.Unmarshal(m.Content, &s); err == nil { if err := json.Unmarshal(m.Content, &s); err == nil {
@@ -92,7 +92,7 @@ func promptFromMessages(msgs []chatMessage) string {
return b.String() return b.String()
} }
// --- handler'lar --- // --- handlers ---
func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
var req chatRequest var req chatRequest
@@ -168,7 +168,7 @@ func (s *Server) handleMetrics(w http.ResponseWriter, _ *http.Request) {
s.eng.WriteMetrics(w) s.eng.WriteMetrics(w)
} }
// --- ortak akış --- // --- shared request flow ---
type servePlan struct { type servePlan struct {
prompt Prompt prompt Prompt
@@ -241,7 +241,7 @@ func (s *Server) bufferedResponse(w http.ResponseWriter, ch <-chan Event, p serv
func (s *Server) streamResponse(w http.ResponseWriter, r *http.Request, ch <-chan Event, p servePlan, id string, created int64) { func (s *Server) streamResponse(w http.ResponseWriter, r *http.Request, ch <-chan Event, p servePlan, id string, created int64) {
flusher, ok := w.(http.Flusher) flusher, ok := w.(http.Flusher)
if !ok { if !ok {
writeError(w, http.StatusInternalServerError, "server_error", "streaming desteklenmiyor") writeError(w, http.StatusInternalServerError, "server_error", "streaming not supported")
return return
} }
h := w.Header() h := w.Header()
@@ -256,7 +256,7 @@ func (s *Server) streamResponse(w http.ResponseWriter, r *http.Request, ch <-cha
for { for {
select { select {
case <-r.Context().Done(): case <-r.Context().Done():
// İstemci koptu. Engine reap() ile isteği batch'ten düşürecek. // Client disconnected; the engine's reap() drops it from the batch.
return return
case ev, open := <-ch: case ev, open := <-ch:
if !open { if !open {
@@ -291,7 +291,7 @@ func (s *Server) streamResponse(w http.ResponseWriter, r *http.Request, ch <-cha
func (s *Server) writeChunk(w http.ResponseWriter, v any) { func (s *Server) writeChunk(w http.ResponseWriter, v any) {
b, err := json.Marshal(v) b, err := json.Marshal(v)
if err != nil { if err != nil {
s.log.Error("chunk serileştirilemedi", "err", err) s.log.Error("failed to marshal chunk", "err", err)
return return
} }
fmt.Fprintf(w, "data: %s\n\n", b) fmt.Fprintf(w, "data: %s\n\n", b)
@@ -319,7 +319,7 @@ func chunk(id string, p servePlan, created int64, text *string, finish string, u
out := map[string]any{ out := map[string]any{
"id": id, "object": p.chunkObject, "created": created, "model": p.model, "id": id, "object": p.chunkObject, "created": created, "model": p.model,
} }
// Usage chunk'ında choices boş dizi olur (OpenAI davranışı). // The usage chunk carries an empty choices array (OpenAI behaviour).
if u != nil { if u != nil {
out["choices"] = []map[string]any{} out["choices"] = []map[string]any{}
out["usage"] = u out["usage"] = u
@@ -329,9 +329,9 @@ func chunk(id string, p servePlan, created int64, text *string, finish string, u
return out return out
} }
// usage, mümkünse engine'in raporladığı gerçek sayıları kullanır. // usage prefers the counts the engine reported. res may be nil (cancelled or
// res nil olabilir (istek iptal edildiyse veya hata aldıysa); o durumda // failed request), in which case prompt tokens are derived from the text and
// prompt token sayısı metinden hesaplanır ve cached_tokens bilinmez. // cached_tokens is unknown.
func (s *Server) usage(p Prompt, completion int, res *Result) usage { func (s *Server) usage(p Prompt, completion int, res *Result) usage {
var u usage var u usage
u.PromptTokens = CountTokens(p.Text) u.PromptTokens = CountTokens(p.Text)
+45 -44
View File
@@ -50,7 +50,7 @@ func TestChatCompletionsBuffered(t *testing.T) {
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
t.Fatalf("durum %d", resp.StatusCode) t.Fatalf("status %d", resp.StatusCode)
} }
var out struct { var out struct {
@@ -75,17 +75,17 @@ func TestChatCompletionsBuffered(t *testing.T) {
t.Fatalf("choices=%d", len(out.Choices)) t.Fatalf("choices=%d", len(out.Choices))
} }
if n := len(strings.Fields(out.Choices[0].Message.Content)); n != 7 { if n := len(strings.Fields(out.Choices[0].Message.Content)); n != 7 {
t.Errorf("beklenen 7 token, alınan %d", n) t.Errorf("expected 7 tokens, got %d", n)
} }
if out.Usage.CompletionTokens != 7 { if out.Usage.CompletionTokens != 7 {
t.Errorf("usage.completion_tokens=%d", out.Usage.CompletionTokens) t.Errorf("usage.completion_tokens=%d", out.Usage.CompletionTokens)
} }
if out.Usage.TotalTokens != out.Usage.PromptTokens+out.Usage.CompletionTokens { if out.Usage.TotalTokens != out.Usage.PromptTokens+out.Usage.CompletionTokens {
t.Error("usage toplamı tutarsız") t.Error("usage totals are inconsistent")
} }
} }
// sseEvent, SSE akışından çözülmüş tek bir olay. // sseEvent is one decoded event from an SSE stream.
type sseEvent struct { type sseEvent struct {
raw string raw string
data map[string]any data map[string]any
@@ -107,7 +107,7 @@ func readSSE(t *testing.T, body *bufio.Reader) []sseEvent {
ev := sseEvent{raw: payload} ev := sseEvent{raw: payload}
if payload != "[DONE]" { if payload != "[DONE]" {
if err := json.Unmarshal([]byte(payload), &ev.data); err != nil { if err := json.Unmarshal([]byte(payload), &ev.data); err != nil {
t.Fatalf("SSE yükü çözülemedi: %v (%q)", err, payload) t.Fatalf("could not decode SSE payload: %v (%q)", err, payload)
} }
} }
out = append(out, ev) out = append(out, ev)
@@ -132,10 +132,10 @@ func TestChatCompletionsStreaming(t *testing.T) {
events := readSSE(t, bufio.NewReader(resp.Body)) events := readSSE(t, bufio.NewReader(resp.Body))
if len(events) == 0 { if len(events) == 0 {
t.Fatal("hiç SSE olayı gelmedi") t.Fatal("no SSE events received")
} }
if last := events[len(events)-1]; last.raw != "[DONE]" { if last := events[len(events)-1]; last.raw != "[DONE]" {
t.Errorf("akış [DONE] ile bitmedi: %q", last.raw) t.Errorf("stream did not end with [DONE]: %q", last.raw)
} }
var contentTokens, usageChunks, finishChunks int var contentTokens, usageChunks, finishChunks int
@@ -163,17 +163,17 @@ func TestChatCompletionsStreaming(t *testing.T) {
} }
if contentTokens != 5 { if contentTokens != 5 {
t.Errorf("beklenen 5 içerik chunk'ı, alınan %d", contentTokens) t.Errorf("expected 5 content chunks, got %d", contentTokens)
} }
if finishChunks != 1 { if finishChunks != 1 {
t.Errorf("beklenen 1 finish_reason chunk'ı, alınan %d", finishChunks) t.Errorf("expected 1 finish_reason chunk, got %d", finishChunks)
} }
if usageChunks != 1 { if usageChunks != 1 {
t.Errorf("include_usage istendi ama %d usage chunk'ı geldi", usageChunks) t.Errorf("include_usage was requested but got %d usage chunks", usageChunks)
} }
} }
func TestStreamingUsageIstenmezseGonderilmez(t *testing.T) { func TestStreamingOmitsUsageUnlessRequested(t *testing.T) {
srv := newTestServer(t, testConfig()) srv := newTestServer(t, testConfig())
resp := postJSON(t, srv.URL+"/v1/chat/completions", map[string]any{ resp := postJSON(t, srv.URL+"/v1/chat/completions", map[string]any{
@@ -188,13 +188,14 @@ func TestStreamingUsageIstenmezseGonderilmez(t *testing.T) {
continue continue
} }
if _, ok := ev.data["usage"]; ok { if _, ok := ev.data["usage"]; ok {
t.Error("istenmediği halde usage chunk'ı gönderildi") t.Error("usage chunk sent although it was not requested")
} }
} }
} }
func TestIstemciKopmasiEngineeYansir(t *testing.T) { func TestClientDisconnectReachesEngine(t *testing.T) {
// Faz 1'in çıkış kriterinin temeli: istemci koptuğunda GPU boşa çalışmamalı. // The basis of phase 1's exit criterion: when a client disconnects the GPU
// must stop working on that request.
cfg := testConfig() cfg := testConfig()
cfg.DecodeTokensPerSec = 20 cfg.DecodeTokensPerSec = 20
eng := New(cfg) eng := New(cfg)
@@ -220,7 +221,7 @@ func TestIstemciKopmasiEngineeYansir(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// İlk token'ı bekle, sonra istemciyi kopar. // Wait for the first token, then drop the client.
waitFor(t, 2*time.Second, func() bool { return eng.Stats().Running == 1 }) waitFor(t, 2*time.Second, func() bool { return eng.Stats().Running == 1 })
cancel() cancel()
_ = resp.Body.Close() _ = resp.Body.Close()
@@ -231,7 +232,7 @@ func TestIstemciKopmasiEngineeYansir(t *testing.T) {
}) })
} }
func TestCompletionsUcNoktasi(t *testing.T) { func TestCompletionsEndpoint(t *testing.T) {
srv := newTestServer(t, testConfig()) srv := newTestServer(t, testConfig())
resp := postJSON(t, srv.URL+"/v1/completions", map[string]any{ resp := postJSON(t, srv.URL+"/v1/completions", map[string]any{
@@ -252,11 +253,11 @@ func TestCompletionsUcNoktasi(t *testing.T) {
t.Errorf("object=%q", out.Object) t.Errorf("object=%q", out.Object)
} }
if n := len(strings.Fields(out.Choices[0].Text)); n != 3 { if n := len(strings.Fields(out.Choices[0].Text)); n != 3 {
t.Errorf("beklenen 3 token, alınan %d", n) t.Errorf("expected 3 tokens, got %d", n)
} }
} }
func TestModelsVeHealth(t *testing.T) { func TestModelsAndHealth(t *testing.T) {
cfg := testConfig() cfg := testConfig()
cfg.Model = "llama-3.3-70b" cfg.Model = "llama-3.3-70b"
srv := newTestServer(t, cfg) srv := newTestServer(t, cfg)
@@ -267,7 +268,7 @@ func TestModelsVeHealth(t *testing.T) {
} }
resp.Body.Close() resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
t.Errorf("/health durum %d", resp.StatusCode) t.Errorf("/health status %d", resp.StatusCode)
} }
resp, err = http.Get(srv.URL + "/v1/models") //nolint:noctx // test resp, err = http.Get(srv.URL + "/v1/models") //nolint:noctx // test
@@ -285,17 +286,17 @@ func TestModelsVeHealth(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if len(out.Data) != 1 || out.Data[0].ID != "llama-3.3-70b" { if len(out.Data) != 1 || out.Data[0].ID != "llama-3.3-70b" {
t.Errorf("/v1/models beklenen modeli listelemedi: %+v", out.Data) t.Errorf("/v1/models did not list the expected model: %+v", out.Data)
} }
} }
// TestMetriklerPrometheusIleParseEdilebilir, §7.6'daki engine kontratını doğrular. // TestMetricsParseWithPrometheus verifies the engine contract from plan §7.6.
// Router bu metrikleri gerçek bir Prometheus parser'ıyla okuyacak; format // The router reads these metrics with a real Prometheus parser; if the format
// bozuksa yönlendirme kör kalır. // is wrong, routing goes blind.
func TestMetriklerPrometheusIleParseEdilebilir(t *testing.T) { func TestMetricsParseWithPrometheus(t *testing.T) {
srv := newTestServer(t, testConfig()) srv := newTestServer(t, testConfig())
// Biraz trafik üret ki sayaçlar dolsun. // Generate some traffic so the counters are non-zero.
resp := postJSON(t, srv.URL+"/v1/chat/completions", map[string]any{ resp := postJSON(t, srv.URL+"/v1/chat/completions", map[string]any{
"messages": []map[string]string{{"role": "user", "content": "merhaba dunya"}}, "messages": []map[string]string{{"role": "user", "content": "merhaba dunya"}},
"max_tokens": 4, "max_tokens": 4,
@@ -308,26 +309,26 @@ func TestMetriklerPrometheusIleParseEdilebilir(t *testing.T) {
} }
defer mresp.Body.Close() defer mresp.Body.Close()
// TextParser'ın sıfır değeri geçersiz; şema açıkça verilmeli. // TextParser's zero value is invalid; the scheme must be given explicitly.
// Legacy şema daha sıkı: metrik adlarımızın klasik Prometheus kurallarına // The legacy scheme is stricter, so this also checks our metric names obey
// uyduğunu da doğrulamış oluyoruz (vLLM adlarındaki ':' legacy'de de geçerli). // classic Prometheus rules (the ':' in vLLM names is legal there too).
parser := expfmt.NewTextParser(model.LegacyValidation) parser := expfmt.NewTextParser(model.LegacyValidation)
families, err := parser.TextToMetricFamilies(mresp.Body) families, err := parser.TextToMetricFamilies(mresp.Body)
if err != nil { if err != nil {
t.Fatalf("metrikler parse edilemedi: %v", err) t.Fatalf("metrics failed to parse: %v", err)
} }
// §7.6 zorunlu üçlüsü. // The three metrics plan §7.6 makes mandatory.
for _, name := range []string{ for _, name := range []string{
"vllm:num_requests_running", "vllm:num_requests_running",
"vllm:num_requests_waiting", "vllm:num_requests_waiting",
"vllm:kv_cache_usage_perc", "vllm:kv_cache_usage_perc",
} { } {
if _, ok := families[name]; !ok { if _, ok := families[name]; !ok {
t.Errorf("zorunlu metrik eksik: %s", name) t.Errorf("mandatory metric missing: %s", name)
} }
} }
// Opsiyoneller ve histogramlar. // Optional metrics and histograms.
for _, name := range []string{ for _, name := range []string{
"vllm:cache_config_info", "vllm:cache_config_info",
"vllm:prefix_cache_queries_total", "vllm:prefix_cache_queries_total",
@@ -335,35 +336,35 @@ func TestMetriklerPrometheusIleParseEdilebilir(t *testing.T) {
"vllm:time_to_first_token_seconds", "vllm:time_to_first_token_seconds",
} { } {
if _, ok := families[name]; !ok { if _, ok := families[name]; !ok {
t.Errorf("metrik eksik: %s", name) t.Errorf("metric missing: %s", name)
} }
} }
if f, ok := families["vllm:generation_tokens_total"]; ok { if f, ok := families["vllm:generation_tokens_total"]; ok {
if got := f.Metric[0].GetCounter().GetValue(); got != 4 { if got := f.Metric[0].GetCounter().GetValue(); got != 4 {
t.Errorf("generation_tokens_total=%v, beklenen 4", got) t.Errorf("generation_tokens_total=%v, expected 4", got)
} }
} else { } else {
t.Error("vllm:generation_tokens_total eksik") t.Error("vllm:generation_tokens_total is missing")
} }
} }
// TestUsageCachedTokensRaporlanir, prefix cache etkisinin istemciye ulaştığını // TestUsageReportsCachedTokens verifies the prefix cache's effect reaches the
// doğrular. Router ileride bunu kullanmayacak olsa da, cache'in gerçekten // client. The router will not consume this, but it is the only end-to-end
// çalıştığını uçtan uca gösteren tek sinyal bu. // signal that the cache actually works.
func TestUsageCachedTokensRaporlanir(t *testing.T) { func TestUsageReportsCachedTokens(t *testing.T) {
srv := newTestServer(t, testConfig()) srv := newTestServer(t, testConfig())
body := map[string]any{ body := map[string]any{
"messages": []map[string]string{{"role": "user", "content": strings.Repeat("ortak onek. ", 100)}}, "messages": []map[string]string{{"role": "user", "content": strings.Repeat("shared prefix. ", 100)}},
"max_tokens": 3, "max_tokens": 3,
} }
// İlk istek cache'i doldurur. // The first request populates the cache.
resp := postJSON(t, srv.URL+"/v1/chat/completions", body) resp := postJSON(t, srv.URL+"/v1/chat/completions", body)
resp.Body.Close() resp.Body.Close()
// İkincisi aynı ön eki kullanmalı. // The second one must reuse that prefix.
resp = postJSON(t, srv.URL+"/v1/chat/completions", body) resp = postJSON(t, srv.URL+"/v1/chat/completions", body)
defer resp.Body.Close() defer resp.Body.Close()
@@ -374,10 +375,10 @@ func TestUsageCachedTokensRaporlanir(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
if out.Usage.PromptTokensDetails.CachedTokens == 0 { if out.Usage.PromptTokensDetails.CachedTokens == 0 {
t.Error("ikinci istekte cached_tokens 0; prefix cache istemciye raporlanmıyor") t.Error("cached_tokens is 0 on the second request; prefix cache is not reported")
} }
if out.Usage.PromptTokensDetails.CachedTokens > out.Usage.PromptTokens { if out.Usage.PromptTokensDetails.CachedTokens > out.Usage.PromptTokens {
t.Errorf("cached_tokens (%d) prompt_tokens'tan (%d) büyük", t.Errorf("cached_tokens (%d) exceeds prompt_tokens (%d)",
out.Usage.PromptTokensDetails.CachedTokens, out.Usage.PromptTokens) out.Usage.PromptTokensDetails.CachedTokens, out.Usage.PromptTokens)
} }
} }
+7 -7
View File
@@ -2,20 +2,20 @@ package sim
import "strings" import "strings"
// simWords, üretilen sahte çıktının kelime havuzu. // simWords is the word pool the fake output is drawn from.
var simWords = []string{ var simWords = []string{
"alfa", "beta", "gama", "delta", "epsilon", "zeta", "eta", "teta", "alpha", "beta", "gamma", "delta", "epsilon", "zeta", "eta", "theta",
"iota", "kappa", "lambda", "mu", "nu", "ksi", "omikron", "pi", "iota", "kappa", "lambda", "mu", "nu", "xi", "omicron", "pi",
} }
// tokenText, n'inci üretilen token'ın metnini verir (n 1'den başlar). // tokenText returns the text of the n-th generated token (n starts at 1).
// Deterministik olması şart: finish sırasında cache'e yazılan metinle // It must be deterministic: the text written into the cache on completion has
// istemciye akıtılan metin birebir aynı olmalı. // to match exactly what was streamed to the client.
func tokenText(n int) string { func tokenText(n int) string {
return " " + simWords[(n-1)%len(simWords)] return " " + simWords[(n-1)%len(simWords)]
} }
// generatedText, ilk n token'ın birleşimini verir. // generatedText returns the concatenation of the first n tokens.
func generatedText(n int) string { func generatedText(n int) string {
var b strings.Builder var b strings.Builder
b.Grow(n * 7) b.Grow(n * 7)
+10 -10
View File
@@ -2,20 +2,20 @@ package sim
import "hash/fnv" import "hash/fnv"
// TokensPerChar, sahte tokenizer'ın kaç bayta bir token ürettiği. // bytesPerToken is how many bytes the fake tokenizer folds into one token.
// Gerçek BPE tokenizer'lara kaba bir yaklaşım; simülasyon için tek gereken // A crude approximation of real BPE tokenizers; all the simulation needs is
// deterministik ve prefix-kararlı olması. // for it to be deterministic and prefix-stable.
const bytesPerToken = 4 const bytesPerToken = 4
// Token, sahte tokenizer'ın ürettiği token kimliği. // Token is a token id produced by the fake tokenizer.
type Token uint32 type Token uint32
// Tokenize, metni deterministik ve prefix-kararlı şekilde token'lara böler. // Tokenize splits text into tokens deterministically and prefix-stably.
// //
// Prefix-kararlılık kritik: aynı ön eke sahip iki metnin token dizileri de aynı // Prefix stability is essential: two texts sharing a prefix must produce token
// ön ekle başlamalı, yoksa prefix cache simülasyonu anlamsız olur. Bu yüzden // sequences sharing that prefix, otherwise the prefix cache simulation is
// bölme, metnin başından itibaren sabit bayt sayısıyla yapılır — içeriğe bağlı // meaningless. That is why splitting happens at fixed byte offsets from the
// (BPE gibi) bir bölme kullanılmaz. // start rather than content-dependent (BPE-style) boundaries.
func Tokenize(s string) []Token { func Tokenize(s string) []Token {
if s == "" { if s == "" {
return nil return nil
@@ -31,7 +31,7 @@ func Tokenize(s string) []Token {
return out return out
} }
// CountTokens, metnin token sayısını tokenize etmeden verir. // CountTokens returns the token count for a text without tokenizing it.
func CountTokens(s string) int { func CountTokens(s string) int {
return (len(s) + bytesPerToken - 1) / bytesPerToken return (len(s) + bytesPerToken - 1) / bytesPerToken
} }
+6 -6
View File
@@ -2,21 +2,21 @@ package sim
import "testing" import "testing"
func TestTokenizeOnEkKararli(t *testing.T) { func TestTokenizeIsPrefixStable(t *testing.T) {
a := Tokenize("merhaba") a := Tokenize("merhaba")
b := Tokenize("merhaba dunya") b := Tokenize("merhaba dunya")
if len(a) > len(b) { if len(a) > len(b) {
t.Fatal("uzun metin daha az token verdi") t.Fatal("longer text produced fewer tokens")
} }
for i := range a[:len(a)-1] { // son token yarım olabilir for i := range a[:len(a)-1] { // the last token may be partial
if a[i] != b[i] { if a[i] != b[i] {
t.Fatalf("token %d ön ek kararlı değil", i) t.Fatalf("token %d is not prefix stable", i)
} }
} }
} }
func TestCountTokensTokenizeIleTutarli(t *testing.T) { func TestCountTokensMatchesTokenize(t *testing.T) {
for _, s := range []string{"", "a", "abcd", "abcde", "uzunca bir prompt metni"} { for _, s := range []string{"", "a", "abcd", "abcde", "a somewhat longer prompt"} {
if got, want := CountTokens(s), len(Tokenize(s)); got != want { if got, want := CountTokens(s), len(Tokenize(s)); got != want {
t.Errorf("%q: CountTokens=%d Tokenize=%d", s, got, want) t.Errorf("%q: CountTokens=%d Tokenize=%d", s, got, want)
} }
+2 -2
View File
@@ -1,5 +1,5 @@
// Package version, build sırasında ldflags ile doldurulan sürüm bilgisini tutar. // Package version holds build information injected via ldflags.
package version package version
// Version, `make build` tarafından git describe çıktısıyla doldurulur. // Version is filled in by `make build` from git describe output.
var Version = "dev" var Version = "dev"
+564 -531
View File
File diff suppressed because it is too large. Load diff