Bug cache pada speculative decoding sering terlihat seperti masalah model, padahal akar masalahnya ada di backend: request berbeda dianggap sama karena cache key terlalu sempit. Akibatnya, hasil prediksi bisa tertukar antar konfigurasi seperti draft model, temperature, max_tokens, atau versi tokenizer.
Di layanan inferensi/training yang mengadopsi pola seperti DeepSpec, bug ini berbahaya karena efeknya tidak selalu berupa error yang eksplisit. Sistem tetap merespons cepat, tetapi output menjadi tidak konsisten, evaluasi eksperimen bias, dan debugging model menjadi menyesatkan. Artikel ini membahas cara mengenali gejala, mereproduksi masalah, menemukan root cause, lalu memperbaikinya secara bertahap di backend Python.
Kenapa bug ini sering lolos ke production
Pada speculative decoding, satu request tidak hanya ditentukan oleh prompt. Ada beberapa parameter yang ikut menentukan token akhir, misalnya:
- model utama yang melakukan verifikasi token,
- draft model yang menghasilkan kandidat token,
- parameter sampling seperti temperature,
- batas generasi seperti max_tokens,
- versi tokenizer atau normalisasi input,
- opsi decoding lain yang memengaruhi distribusi atau terminasi.
Masalah muncul ketika layer cache hanya memakai sebagian kecil konteks, misalnya prompt_hash atau model_name + prompt. Dari sudut pandang aplikasi, dua request memang terlihat mirip. Dari sudut pandang algoritme, keduanya bisa menghasilkan output yang sah tetapi berbeda. Jika cache menganggapnya identik, sistem akan mengembalikan hasil lama untuk request baru.
Inilah yang membuat bug ini sulit dideteksi. Tidak ada exception, tidak ada timeout, tidak ada crash. Yang muncul justru gejala halus: eksperimen tampak "flaky", akurasi turun tanpa pola jelas, atau hasil produksi tidak cocok dengan replay offline.
Gejala di production
Dalam praktik backend Python, gejala cache salah untuk hasil speculative decoding biasanya muncul sebagai kombinasi beberapa sinyal berikut:
1. Output identik padahal parameter berbeda
Tim mengubah draft model atau temperature, tetapi output tetap persis sama lebih sering dari yang masuk akal. Ini patut dicurigai jika cache hit rate naik bersamaan.
2. Hasil replay offline tidak cocok dengan request production
Request yang diekspor dari production lalu dijalankan ulang secara offline memberi hasil berbeda, meskipun prompt terlihat sama. Sering kali penyebabnya adalah metadata konfigurasi tidak ikut dalam cache key atau tidak tercatat di log.
3. Evaluasi eksperimen tampak bias
Satu eksperimen terlihat lebih baik hanya karena banyak mengambil hasil dari cache eksperimen lain. Akibatnya, perbandingan antar konfigurasi tidak lagi valid.
4. Distribusi panjang output aneh
Misalnya request dengan max_tokens=32 kadang menerima hasil yang jelas berasal dari request max_tokens=128. Jika response sudah dipotong di layer lain, bug ini bisa lebih sulit dilihat.
5. Metrik cache terlalu bagus
Cache hit rate yang sangat tinggi untuk workload dengan variasi parameter besar justru bisa menjadi red flag. Angka yang terlalu bagus belum tentu sehat.
Studi kasus minimal: cache key hanya berbasis prompt
Misalkan sebuah service Python menyimpan hasil speculative decoding di Redis atau in-memory cache. Implementasi awalnya tampak masuk akal, tetapi salah secara semantik:
from dataclasses import dataclass
from typing import Any
@dataclass
class DecodeRequest:
prompt: str
target_model: str
draft_model: str
temperature: float
max_tokens: int
tokenizer_version: str
class DecodeService:
def __init__(self, cache, engine):
self.cache = cache
self.engine = engine
def _cache_key(self, req: DecodeRequest) -> str:
# SALAH: hanya prompt yang dipakai
return f"specdec:{hash(req.prompt)}"
def generate(self, req: DecodeRequest) -> dict[str, Any]:
key = self._cache_key(req)
cached = self.cache.get(key)
if cached is not None:
return cached
result = self.engine.generate(
prompt=req.prompt,
target_model=req.target_model,
draft_model=req.draft_model,
temperature=req.temperature,
max_tokens=req.max_tokens,
tokenizer_version=req.tokenizer_version,
)
self.cache.set(key, result, ttl=300)
return resultKode di atas bermasalah karena semua request dengan prompt sama akan berbagi hasil, padahal konfigurasi decoding bisa berbeda. Pada beban riil, bug ini dapat mencampur hasil antar eksperimen, antar model draft, bahkan antar versi tokenizer setelah deploy.
Langkah reproduksi minimal
Reproduksi minimal penting agar tim yakin ini bug backend, bukan variasi normal dari model.
Skenario reproduksi
- Kirim request A dengan prompt yang sama,
draft_model=draft-small,temperature=0.2,max_tokens=32. - Simpan hasilnya ke cache.
- Kirim request B dengan prompt yang sama, tetapi
draft_model=draft-largeatautemperature=0.8. - Amati bahwa service mengembalikan hasil request A karena key cache sama.
Contoh reproduksi sederhana:
def test_wrong_cache_reuse():
cache = {}
class DictCache:
def get(self, key):
return cache.get(key)
def set(self, key, value, ttl=None):
cache[key] = value
class FakeEngine:
def generate(self, **kwargs):
return {
"text": f"draft={kwargs['draft_model']};temp={kwargs['temperature']}",
"meta": kwargs,
}
service = DecodeService(DictCache(), FakeEngine())
req_a = DecodeRequest(
prompt="Jelaskan cache bug",
target_model="target-v1",
draft_model="draft-small",
temperature=0.2,
max_tokens=32,
tokenizer_version="tok-v1",
)
req_b = DecodeRequest(
prompt="Jelaskan cache bug",
target_model="target-v1",
draft_model="draft-large",
temperature=0.8,
max_tokens=32,
tokenizer_version="tok-v1",
)
result_a = service.generate(req_a)
result_b = service.generate(req_b)
assert result_a["text"] != result_b["text"] # akan gagal bila cache key salahJika assertion gagal, Anda sudah punya bukti konkret bahwa cache mengembalikan hasil untuk konteks request yang berbeda.
Log dan metrik yang benar-benar membantu
Saat bug sudah terjadi di production, observabilitas menjadi faktor penentu. Tanpa metadata yang cukup, tim cenderung menyalahkan model atau dataset.
Log yang sebaiknya ada
- cache_key yang dipakai service,
- cache_hit/cache_miss,
- prompt_hash alih-alih prompt mentah jika ada isu privasi,
- target_model dan draft_model,
- temperature, max_tokens,
- tokenizer_version,
- opsi decoding penting lain yang memengaruhi hasil,
- checksum atau ringkasan dari response untuk mendeteksi reuse yang janggal.
Contoh logging terstruktur:
logger.info(
"spec_decode_request",
extra={
"cache_key": key,
"cache_hit": cached is not None,
"prompt_hash": prompt_hash,
"target_model": req.target_model,
"draft_model": req.draft_model,
"temperature": req.temperature,
"max_tokens": req.max_tokens,
"tokenizer_version": req.tokenizer_version,
},
)Metrik yang layak dipantau
- Cache hit rate per kombinasi model: hit rate yang tinggi pada konfigurasi yang seharusnya beragam patut dicurigai.
- Mismatch rate antara metadata request dan metadata hasil cache jika metadata response disimpan.
- Output length distribution per
max_tokens. - Unique cache keys / unique requests: rasio terlalu kecil bisa menandakan key terlalu kasar.
- Evaluation drift: selisih hasil offline vs production replay.
Tip praktis: simpan sebagian metadata request di value cache, bukan hanya di key. Ini memudahkan deteksi mismatch saat response dibaca kembali.
Root cause: key cache tidak merepresentasikan identitas request
Akar masalahnya bukan sekadar "lupa menambah field". Secara desain, cache key gagal merepresentasikan semantic identity dari operasi decoding. Dalam sistem backend, cache aman hanya jika dua request dengan key yang sama memang boleh berbagi hasil tanpa mengubah makna.
Pada speculative decoding, identitas request biasanya mencakup:
- isi input setelah normalisasi yang relevan,
- model utama,
- draft model,
- parameter sampling yang memengaruhi token,
- batas generasi seperti
max_tokens, - versi tokenizer atau preprocessor,
- versi implementasi service jika format hasil berubah.
Kesalahan umum lain adalah memakai representasi non-deterministik untuk membangun key, misalnya serialisasi dictionary tanpa pengurutan stabil, pembulatan float yang tidak konsisten, atau pemakaian hash() bawaan Python untuk persistensi lintas proses. hash() Python tidak cocok untuk key cache yang harus stabil antar proses atau restart.
Dampak ke akurasi dan evaluasi eksperimen
Bug ini tidak hanya mengotori cache, tetapi juga mengganggu keputusan teknis.
Dampak pada inferensi production
- Pengguna menerima hasil yang tidak sesuai konfigurasi request.
- Perubahan model draft tampak tidak memberi pengaruh, padahal hasil lama yang dikembalikan.
- Rollback atau deploy tokenizer baru terlihat aman padahal cache masih menyajikan output dari versi lama.
Dampak pada training dan evaluasi
- Eksperimen A bisa mengambil hasil eksperimen B.
- Perbandingan kualitas antar setting decoding menjadi tidak valid.
- Dataset evaluasi menjadi terkontaminasi hasil cache lama.
- Tim bisa salah menyimpulkan bahwa perubahan model berhasil atau gagal.
Inilah alasan bug cache pada layanan inferensi/training speculative decoding harus diperlakukan sebagai isu correctness, bukan sekadar isu performa.
Perbaikan bertahap yang aman
Perbaikan terbaik biasanya tidak dilakukan dalam satu commit besar. Mulailah dari desain key, lanjut ke invalidasi, guard di layer service, lalu regression test.
1. Desain cache key yang deterministik
Prinsipnya: key harus dibentuk dari semua parameter yang memengaruhi hasil, dengan serialisasi stabil dan mudah diaudit.
import hashlib
import json
from dataclasses import asdict
class SafeDecodeService:
CACHE_SCHEMA_VERSION = "v2"
def __init__(self, cache, engine):
self.cache = cache
self.engine = engine
def _normalized_payload(self, req: DecodeRequest) -> dict:
return {
"schema": self.CACHE_SCHEMA_VERSION,
"prompt": req.prompt,
"target_model": req.target_model,
"draft_model": req.draft_model,
"temperature": round(req.temperature, 6),
"max_tokens": req.max_tokens,
"tokenizer_version": req.tokenizer_version,
}
def _cache_key(self, req: DecodeRequest) -> str:
payload = self._normalized_payload(req)
raw = json.dumps(payload, sort_keys=True, separators=(",", ":"))
digest = hashlib.sha256(raw.encode("utf-8")).hexdigest()
return f"specdec:{self.CACHE_SCHEMA_VERSION}:{digest}"
def generate(self, req: DecodeRequest):
key = self._cache_key(req)
cached = self.cache.get(key)
if cached is not None:
return cached
result = self.engine.generate(**self._normalized_payload(req))
wrapped = {
"request_fingerprint": self._normalized_payload(req),
"result": result,
}
self.cache.set(key, wrapped, ttl=300)
return wrappedKenapa pendekatan ini bekerja:
- Deterministik:
json.dumps(..., sort_keys=True)menghasilkan representasi stabil. - Auditabel: payload yang dipakai untuk fingerprint bisa dicatat di log.
- Aman lintas proses: memakai digest kriptografis, bukan
hash()Python. - Mudah dievolusi: ada
schema versionuntuk perubahan format key.
Trade-off: semakin lengkap key, semakin rendah peluang cache hit. Tetapi untuk kasus correctness seperti ini, hit rate yang sedikit turun lebih baik daripada hasil yang salah.
2. Strategi invalidasi cache
Setelah desain key diperbaiki, cache lama belum tentu aman. Anda perlu strategi invalidasi yang jelas:
- Bump schema version saat format key atau semantik request berubah.
- TTL yang konservatif untuk hasil inferensi yang sensitif terhadap perubahan model/tokenizer.
- Namespace per deployment jika sering ada rollout model atau tokenizer.
- Warm-up selektif hanya untuk request yang memang stabil dan sering dipakai.
Kesalahan umum adalah memperbaiki key tanpa memisahkan cache lama. Akibatnya, masih ada data historis yang ikut terbaca dengan asumsi lama.
3. Guard di layer service
Selain memperbaiki key, tambahkan guard agar service bisa mendeteksi mismatch lebih awal. Misalnya, saat membaca dari cache, cek apakah metadata yang tersimpan cocok dengan request saat ini.
def generate(self, req: DecodeRequest):
key = self._cache_key(req)
cached = self.cache.get(key)
expected = self._normalized_payload(req)
if cached is not None:
fingerprint = cached.get("request_fingerprint")
if fingerprint == expected:
return cached["result"]
logger.warning(
"cache_fingerprint_mismatch",
extra={"cache_key": key, "expected": expected, "actual": fingerprint},
)
# Opsi aman: abaikan cache lalu hit engine
result = self.engine.generate(**expected)
self.cache.set(key, {"request_fingerprint": expected, "result": result}, ttl=300)
return resultGuard ini membantu untuk dua hal:
- mendeteksi inkonsistensi akibat bug lama atau data cache korup,
- memberi sinyal observabilitas sebelum masalah terlihat di akurasi bisnis.
4. Regression test yang spesifik
Jangan puas dengan unit test dasar. Tambahkan regression test yang benar-benar menangkap kelas bug ini.
Minimal ada empat skenario:
- Prompt sama, draft model berbeda harus menghasilkan key berbeda.
- Prompt sama, temperature berbeda harus menghasilkan key berbeda.
- Prompt sama, max_tokens berbeda harus menghasilkan key berbeda.
- Prompt sama, tokenizer_version berbeda harus menghasilkan key berbeda.
Contoh singkat:
def test_cache_key_changes_when_draft_model_changes():
service = SafeDecodeService(cache=None, engine=None)
a = DecodeRequest("p", "target", "draft-a", 0.2, 32, "tok-v1")
b = DecodeRequest("p", "target", "draft-b", 0.2, 32, "tok-v1")
assert service._cache_key(a) != service._cache_key(b)
def test_cache_key_changes_when_tokenizer_version_changes():
service = SafeDecodeService(cache=None, engine=None)
a = DecodeRequest("p", "target", "draft-a", 0.2, 32, "tok-v1")
b = DecodeRequest("p", "target", "draft-a", 0.2, 32, "tok-v2")
assert service._cache_key(a) != service._cache_key(b)Jika service mendukung banyak opsi decoding, buat satu test parametrik untuk semua field penting agar penambahan parameter baru tidak lupa masuk ke key.
Common mistakes saat memperbaiki bug ini
Hanya menambahkan satu field yang kebetulan terlihat bermasalah
Misalnya menambah draft_model tetapi tetap melupakan tokenizer_version. Ini hanya memindahkan bug, bukan menghilangkannya.
Terlalu agresif membulatkan float
Membulatkan temperature memang berguna untuk stabilitas serialisasi, tetapi jika pembulatan terlalu kasar, request yang semestinya berbeda bisa bertabrakan lagi.
Menyimpan prompt mentah tanpa mempertimbangkan privasi
Untuk log, lebih aman simpan prompt hash atau bentuk redaksi terbatas. Untuk cache key internal, prompt mentah boleh dipakai sebagai material digest selama penyimpanan dan kontrol aksesnya tepat.
Mengandalkan invalidasi manual
Jika perubahan model/tokenizer sering terjadi, invalidasi manual mudah terlewat. Lebih aman gunakan namespace atau schema version yang eksplisit.
Mengukur sukses hanya dari hit rate
Perbaikan yang benar bisa menurunkan hit rate. Ukuran sukses utama adalah correctness: output sesuai request, evaluasi bersih, dan replay konsisten.
Checklist debugging di backend Python
Saat menghadapi dugaan cache salah untuk hasil speculative decoding, gunakan checklist ini:
- Bandingkan dua request dengan prompt sama tetapi parameter decoding berbeda.
- Periksa apakah key cache berubah untuk setiap parameter penting.
- Audit penggunaan
hash(), serialisasi dict, dan normalisasi float. - Pastikan metadata request ikut tersimpan pada value cache.
- Tambahkan log
cache_hit,cache_key,draft_model,temperature,max_tokens, dantokenizer_version. - Lakukan replay offline dari request production dengan metadata lengkap.
- Invalidasi namespace lama setelah skema key diperbaiki.
- Tambahkan regression test untuk semua field yang memengaruhi hasil.
Penutup
Pada layanan inferensi/training speculative decoding, cache bukan sekadar optimasi performa. Ia adalah bagian dari correctness pipeline. Jika cache key tidak memasukkan parameter penting seperti draft model, temperature, max_tokens, atau versi tokenizer, hasil prediksi bisa tertukar tanpa gejala error yang jelas.
Pendekatan yang paling aman adalah mendesain cache key secara deterministik, memisahkan namespace saat semantik berubah, menambahkan guard di layer service, dan mengunci perilaku dengan regression test. Dengan begitu, tim backend Python bisa membedakan masalah model yang nyata dari bug cache yang menyamar sebagai penurunan akurasi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!