Plan v0.2'nin Faz 0 çıktısı. Yazılan sıraya göre:
- cobra tabanlı CLI iskeleti; server/agent/ctl komutları henüz stub.
- internal/sim: OpenAI uyumlu sahte inference engine. Router'ı GPU olmadan
gerçekçi koşullarda test edebilmek için gerçek bir engine'in router
açısından önemli davranışlarını modeller:
* continuous batching ve KV blok muhasebesi (head-of-line blocking dahil)
* zincirlenmiş blok hash'leri + LRU ile otomatik prefix cache
* batch büyüdükçe artan token gecikmesi
* istemci koptuğunda isteğin düşürülüp blokların bırakılması
* vLLM metrik adlarıyla /metrics (plan §7.6 engine kontratı)
- Testler: prefix hash zincirlemesi, LRU tahliyesi, iptal propagasyonu,
kuyruklama, SSE formatı, usage/cached_tokens, metriklerin gerçek
Prometheus parser'ıyla okunabilirliği.
- Gitea Actions CI: gofmt, vet, race detector.
Modül yolu gitea.mynodes.xyz/ilker/llmgw; `make rename` ile değiştirilebilir.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
129 lines
3.3 KiB
Go
129 lines
3.3 KiB
Go
package sim
|
||
|
||
import (
|
||
"container/list"
|
||
"encoding/binary"
|
||
"hash/fnv"
|
||
)
|
||
|
||
// BlockHash, KV cache'in muhasebe birimi olan bir bloğun kimliği.
|
||
type BlockHash uint64
|
||
|
||
// BlockHashes, token dizisi için zincirlenmiş blok hash'leri üretir.
|
||
//
|
||
// Zincirleme (h_i = hash(h_{i-1}, block_i)) vLLM'in yaptığının aynısıdır ve
|
||
// şart: blok kimliği sadece kendi içeriğini değil, kendisinden önce gelen tüm
|
||
// blokları da temsil etmeli. Aksi halde "aynı cümleyi farklı bağlamda içeren"
|
||
// iki istek yanlışlıkla cache hit sayılır.
|
||
//
|
||
// Son eksik blok yok sayılır — yarım blok cache'lenmez.
|
||
func BlockHashes(tokens []Token, blockSize int) []BlockHash {
|
||
if blockSize <= 0 || len(tokens) < blockSize {
|
||
return nil
|
||
}
|
||
n := len(tokens) / blockSize
|
||
out := make([]BlockHash, 0, n)
|
||
var prev BlockHash
|
||
buf := make([]byte, 8)
|
||
for i := range n {
|
||
h := fnv.New64a()
|
||
binary.LittleEndian.PutUint64(buf, uint64(prev))
|
||
_, _ = h.Write(buf)
|
||
for _, t := range tokens[i*blockSize : (i+1)*blockSize] {
|
||
binary.LittleEndian.PutUint32(buf[:4], uint32(t))
|
||
_, _ = h.Write(buf[:4])
|
||
}
|
||
prev = BlockHash(h.Sum64())
|
||
out = append(out, prev)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// PrefixCache, engine'in otomatik prefix cache'ini modelleyen blok bazlı LRU.
|
||
//
|
||
// Gerçek engine'de bu bir radix tree'dir; ama yönlendirme kalitesini ölçmek için
|
||
// önemli olan davranış "hangi blokların hâlâ cache'te olduğu"dur ve onu LRU
|
||
// doğru modeller.
|
||
type PrefixCache struct {
|
||
capacity int // blok cinsinden
|
||
ll *list.List
|
||
m map[BlockHash]*list.Element
|
||
|
||
queries int64
|
||
hits int64
|
||
}
|
||
|
||
// NewPrefixCache, verilen blok kapasitesinde bir cache kurar.
|
||
// capacity <= 0 ise cache devre dışıdır (her zaman miss).
|
||
func NewPrefixCache(capacity int) *PrefixCache {
|
||
return &PrefixCache{
|
||
capacity: capacity,
|
||
ll: list.New(),
|
||
m: make(map[BlockHash]*list.Element),
|
||
}
|
||
}
|
||
|
||
// Enabled, cache'in açık olup olmadığını söyler.
|
||
func (c *PrefixCache) Enabled() bool { return c != nil && c.capacity > 0 }
|
||
|
||
// Lookup, hashes dizisinin cache'te bulunan **kesintisiz ön ek** uzunluğunu
|
||
// blok cinsinden döner ve bulunan blokları LRU'da tazeler.
|
||
//
|
||
// Kesintisiz olması önemli: ortadaki bir blok düşmüşse ondan sonrası da
|
||
// yeniden hesaplanmak zorundadır, çünkü attention önceki tüm bloklara bağlıdır.
|
||
func (c *PrefixCache) Lookup(hashes []BlockHash) int {
|
||
if !c.Enabled() {
|
||
return 0
|
||
}
|
||
c.queries += int64(len(hashes))
|
||
n := 0
|
||
for _, h := range hashes {
|
||
el, ok := c.m[h]
|
||
if !ok {
|
||
break
|
||
}
|
||
c.ll.MoveToFront(el)
|
||
n++
|
||
}
|
||
c.hits += int64(n)
|
||
return n
|
||
}
|
||
|
||
// Insert, blokları cache'e yazar (LRU'nun başına) ve kapasiteyi aşarsa en eskileri atar.
|
||
func (c *PrefixCache) Insert(hashes []BlockHash) {
|
||
if !c.Enabled() {
|
||
return
|
||
}
|
||
for _, h := range hashes {
|
||
if el, ok := c.m[h]; ok {
|
||
c.ll.MoveToFront(el)
|
||
continue
|
||
}
|
||
c.m[h] = c.ll.PushFront(h)
|
||
}
|
||
for c.ll.Len() > c.capacity {
|
||
el := c.ll.Back()
|
||
if el == nil {
|
||
break
|
||
}
|
||
c.ll.Remove(el)
|
||
delete(c.m, el.Value.(BlockHash))
|
||
}
|
||
}
|
||
|
||
// Stats, cache sorgu ve isabet sayılarını döner (blok cinsinden).
|
||
func (c *PrefixCache) Stats() (queries, hits int64) {
|
||
if c == nil {
|
||
return 0, 0
|
||
}
|
||
return c.queries, c.hits
|
||
}
|
||
|
||
// Len, cache'teki blok sayısı.
|
||
func (c *PrefixCache) Len() int {
|
||
if !c.Enabled() {
|
||
return 0
|
||
}
|
||
return c.ll.Len()
|
||
}
|