Token/saniye tek sayı değil, iki sayı

On iki yıl önce elektrik sayacı okuyan bir uygulama yazdım. Sahada endüstriyel Android cihazlar vardı.

O günlerde bir istek gelmişti. Sunucuya gidip gelmeyen bir sistem kur.

Yapamadım. Cihazda çalışacak model yoktu, telefonda o işi kaldıracak işlemci yoktu. İstek mantıklıydı, teknoloji yoktu.

Tekrardan bir şeyler paylaşmaya, yazmaya karar verince geriye dönüp o çözemediğim işlere baktım. O zaman imkânsız olan şey bugün ne kadar ulaşılabilir diye merak ettim.

Aynı işi bugün şöyle düşünün. Teknisyen panonun fotoğrafını çekiyor. Uygulama bunun gerçekten bir elektrik panosu olduğunu, sayacın da elektrik sayacı olduğunu doğruluyor. Sunucuya hiçbir şey gitmiyor.

Bugün arkasına bir API koyabilirim. Ama koymak istemem. İstek sayısı benim kontrolümde değil, kötüye kullanılırsa fatura şişer. Sahada bağlantı çoğu zaman iyi değil, bazen bodrum katındasın bazen telefon çekmeyen bir dağda. Ve saha çalışanı işi hızlı bitirip oradan uzaklaşmak zorunda.

Yani on iki yıl önce imkânsız olan iş, bugün cihazın kendisinde çözülebilir görünüyor.

Ama kurmadan önce üç şeyi bilmem gerekiyor. Bu iş cihazda mı daha hızlı olur yoksa API’de mi. Maliyeti ne olur. Ve kullanıcının eline geçen süre kaç saniye olur.

Birinci soruyu cevaplamak için model kartlarına baktım. Hepsinde tek bir hız sayısı yazıyordu.

O sayı yanlış sayıydı.

Tek sayı yok, iki sayı var

Bir dil modeli çalışırken iki ayrı aşamadan geçiyor. Birincisi promptun okunması. İkincisi cevabın yazılması. Bu iki aşama farklı donanım kaynağına takılıyor, dolayısıyla farklı hızlar üretiyor. Ölçtüğüm dokuz yapılandırmada aradaki fark 5 kat ile 100 kat arasında çıktı. Nerede durduğunuz cihaza, çalışma zamanına ve promptun uzunluğuna bağlı.

Model kartında gördüğünüz tek sayı genelde ikincisi. Yani cevabın yazılma hızı. Eğer sizin işiniz uzun bir metni okutmaksa, o sayının size söyleyeceği pek bir şey yok.

Neden ikisi bu kadar farklı

Prefill aşamasında promptun bütün tokenları aynı anda işleniyor. Donanım bunu tek bir büyük matris çarpımı olarak görüyor. Belleğe bir kez gidip aldığı ağırlıkla çok sayıda hesap yapıyor. Bu yüzden prefill hesap gücüne takılıyor, yani compute bound.

Decode aşamasında ise model tek seferde tek token üretiyor ve ürettiğini bir sonraki adımın girdisi yapıyor. Sıralı bir iş, paralelleştiremiyorsunuz. Her tek token için modelin bütün ağırlıklarının bellekten geçmesi gerekiyor. Hesap az, bellek trafiği çok. Bu yüzden decode bellek bant genişliğine takılıyor, yani memory bound.

Prefill processes every prompt token in one pass and is limited by compute. Decode produces one token per pass and is limited by memory bandwidth. PREFILLreads the promptevery tokenone pass through the modelmatrix times matrixlimited by computethis is the wait before the first tokenDECODEwrites the answerone tokenevery weight read from memoryrepeat for the next tokenmatrix times vectorlimited by memory bandwidththis is how fast the answer flows
Two phases, two different limits. Everything in this post follows from that.

Buradan çıkan kural şu. Decode hızının tavanı kabaca bellek bant genişliğinin, token başına gerçekten okunan bayta bölümü kadar. Buradaki payda model dosyasının boyutu değil. Uzman karışımı mimarilerde token başına ağırlıkların sadece bir kısmı okunur, o yüzden böyle bir model dosya boyutunun vaat ettiğinden çok daha hızlı decode eder. Kendi masaüstü ölçümümde 20 milyar parametreli bir uzman karışımı model, dosya boyutundan hesapladığım tavanın üç katı hızda üretti. Kural yine de yön veriyor. Token başına okunan baytı düşürürseniz decode hızlanır, prefill ise bundan neredeyse etkilenmez.

Nasıl ölçtüm

Üç cihazda ölçtüm. iPhone 15 Pro, Pixel 10 Pro XL ve karşılaştırma için bir MacBook Pro M3 Max. Telefonların ikisi de gerçek cihaz, emülatör yok.

Kendi yazdığım ölçümlerde bir ısınma koşusu attım ve onu saydırmadım, sonra üç koşu alıp medyanı yazdım. Her istemin başına rastgele bir etiket koydum, sebebini birazdan anlatacağım. Token sayılarını kendi tahminimden değil, çalışma zamanının kendi raporundan aldım.

Yöntem her kaynakta aynı değil ve bunu tabloların altında belirttim. llama.cpp tarafında aracın kendi tekrar sayısı ve ortalaması geçerli. Google’ın uygulamasından gelen iki satır ise uygulamanın kendi ortalaması, benim medyanım değil.

Üç iş tipi ölçtüm, çünkü telefondaki her iş sohbet değil.

İş tipiPromptÇıktıBeklemeyi belirleyen
Sınıflandırmakısaçok kısaprefill ve sabit gider
Çıkarımuzunkısaprefill
Üretimkısauzundecode

Pano doğrulama işi birinci satır. Kısa prompt, çok kısa çıktı. Yani o işte beklemeyi belirleyen şey modelin yazma hızı değil.

Telefonda üç iş, üç farklı sonuç

iPhone 15 Pro, Qwen3 1.7B modelinin 4 bit sürümü, MLX ile.

İşPromptİlk tokenDecode
sınıflandırma51 token196 msölçülmedi
çıkarım1692 token6919 ms36.6 tok/s
üretim54 token573 ms29.6 tok/s

Aynı model, aynı telefon. İlk cevabın gelme süresi 196 milisaniye ile 6919 milisaniye arasında değişiyor. Otuz beş kat.

Burada dürüst olmam gereken bir nokta var. Uzunluk tek başına her şeyi açıklamıyor. Üçüncü satıra bakın, 54 tokenlık prompt 573 milisaniye sürüyor, oysa 51 tokenlık prompt 196 milisaniyede bitiyor. Neredeyse aynı prompt, üç kat fark. Aradaki tek yapılandırma farkı üretim bütçesi, birinde 16 token diğerinde 400. Muhtemelen önbellek için ayrılan yer bu süreye giriyor ama izole etmedim, o yüzden sebebini bilmiyorum diyorum.

Tabloda ikinci bir şey daha var. Decode hızı üretim uzadıkça düşüyor. Elli token üretirken 36.6, dört yüz token üretirken 29.6.

İlk açıklamam bağlamın büyümesiydi, ama kendi verim bunu çürütüyor. Çıkarım işinde bağlam 1741 token ve decode 36.6. Üretim işinde bağlam 454 token, yani dört kat küçük, ve decode 29.6. Daha küçük bağlamlı koşu daha yavaş. Geriye en olası aday olarak cihazın ısınması kalıyor, ama termal durumu kaydetmediğim için bunu da ölçmüş sayılmam. Kesin olan tek şey şu: bir modelin decode hızı tek bir sayı değil, üretim uzunluğuna bağlı bir eğri.

TPU ne kazandırıyor

Burası beni en çok şaşırtan kısım oldu.

Pixel 10 Pro XL’de Google’ın AI Edge Gallery uygulamasıyla Gemma 4 E2B modelini iki kez çalıştırdım. Bir koşu CPU üzerinde, bir koşu cihazın TPU çekirdeği üzerinde. Ayarlar aynı, 256 prompt tokenı ve 256 üretim tokenı, üç koşu.

Bu karşılaştırmanın kusurunu baştan söyleyeyim. Uygulama TPU için ayrı bir model dosyası indiriyor, 4.0 GB, CPU dosyası ise 2.6 GB. Yani aynı dosyayı iki çekirdekte koşturamadım, iki build karşılaştırdım. Değişen tek şey çekirdek değil. Ayrıca bu iki sayı benim kronometremden değil, uygulamanın kendi ölçüm ekranından geliyor.

The TPU is 9.7 times faster than the CPU on prefill and 1.07 times on decode, which is inside the run to run noise of the CPU itself. TPU SPEED DIVIDED BY CPU SPEEDPrefill9.70x2005.7 tok/s against 206.7 tok/sDecode1.07x20 tok/s against 18.7 tok/s1x, no differenceThe CPU column is the mean of two runs that differ from each other by 5.5 percent, so the decode bar sits inside that noise. The two accelerators also load different model builds, 2.6 GB against 4.0 GB, so this is a comparison of two paths, not of one file on two cores.
Pixel 10 Pro XL, Gemma 4 E2B through LiteRT-LM, 256 prompt tokens and 256 generated tokens, figures reported by the app.

Prefill tarafında TPU neredeyse on kat hızlı. Decode tarafında kazanç yüzde yedi. Bu kazancın gürültü olduğunu düşünmemin sebebi şu: aynı CPU’yu iki kez ölçtüm ve iki koşu kendi arasında yüzde beş buçuk fark etti. Yani iddia edilen hızlanma, tek bir donanımın tekrar gürültüsü kadar.

Yön mekanizmayla uyumlu. TPU matris çarpımını hızlandırmak için var, prefill de matris çarpımı. Decode ise her token için ağırlıkları bellekten geçiriyor ve TPU bu telefondaki bellek bant genişliğini değiştirmiyor. Not düşeyim, bu genel bir yasa değil. Yeterince büyük yerleşik belleği olan bir hızlandırıcı decode’u da hızlandırabilir. Bu cihazda olmuyor.

Bir de faturası var. TPU yolunda her oturumun hazırlanması 3045 milisaniye sürdü, CPU yolunda 700 milisaniye. Yani modeli sıcak tutuyorsanız TPU açık ara kazanıyor. Her fotoğrafta modeli yeniden yüklüyorsanız kazandığınız saniyeleri hazırlıkta geri veriyorsunuz.

İki telefon ve bir masaüstü

CihazYolPrefillDecode
iPhone 15 ProMLX, Qwen3 1.7B 4 bit245 tok/s36.6 tok/s
Pixel 10 Pro XLllama.cpp CPU, 4 iş parçacığı92.6 tok/s17.4 tok/s
Pixel 10 Pro XLllama.cpp CPU, 6 iş parçacığı107.6 tok/s15.7 tok/s
Pixel 10 Pro XLllama.cpp Vulkan GPU, aynı model24 tok/s2.5 tok/s
MacBook Pro M3 Maxllama.cpp Metal, aynı model3566 tok/s207 tok/s

Pixel’in iki satırına dikkat edin, ikisi aynı cihaz ve aynı model. Sadece iş parçacığı sayısı değişiyor. Çekirdek sayısını artırınca prefill yükseliyor, decode ise düşüyor. Yazının tezi burada tek tabloda görünüyor. Bu cihazın tek bir hız sayısı yok, hangi fazı önemsediğinize göre farklı ayar kazanıyor.

Satırlar farklı çalışma zamanları kullanıyor, o yüzden cihazları birbiriyle yarıştırmak doğru olmaz. Doğru okuma her satırı kendi içinde okumak. Bir satır daha ilginç.

Pixel’de genel amaçlı GPU yolu, yani Vulkan, prefill’de CPU’dan 4.5 kat, decode’da 7 kat yavaş çıktı. Sürücü açılışta kendi yeteneklerini yazıyor ve tam sayı nokta çarpımı ile matris çekirdeği desteklemediğini bildiriyor. Bu, prefill tarafındaki farkı açıklıyor. Decode tarafındaki 7 katı açıklamıyor, çünkü decode zaten hesaba bağlı değil ve bu GPU ile CPU aynı belleği paylaşıyor. Oradaki farkın sebebini bilmiyorum, sürücü olgunluğu ya da shader verimi olabilir, ölçmedim.

Google’ın kendi uygulaması da bu cihazda GPU seçeneği sunmuyor, sadece CPU ve TPU. Bu bir kanıt değil ama aynı yöne işaret eden bir ipucu.

Yani Android’de “GPU’ya ver, hızlanır” varsayımı bu cihazda yanlış.

Modeli küçültmek hangi tarafı hızlandırır

Masaüstünde aynı modeli iki farklı nicelemede ölçtüm.

ModelBoyutPrefillDecode
Qwen3 1.7B, 4 bit1.03 GB3566 tok/s207 tok/s
Qwen3 1.7B, 8 bit1.70 GB3729 tok/s150 tok/s

Dosya 1.70 GB’tan 1.03 GB’a iniyor, yani yüzde 39 küçülüyor. Decode yüzde 38 hızlanıyor. Prefill ise yüzde 4.6 düşüyor, yani küçük model orada hafifçe geride kalıyor. Bellek sınırı ile hesap sınırı tek tabloda.

Saf bant genişliği hesabı burada yüzde 65 kazanç öngörürdü, ölçülen yüzde 38. Yani kural yönü veriyor, ama tek başına yeterli değil.

Aynı yön telefonda daha da belirgin. Pixel’de 6 iş parçacığıyla Q4 sürümü prefill’de 107.6 tok/s okudu, Q8 sürümü 138.1 tok/s. Decode tarafında sıralama tersine dönüyor, Q4 17.4 ve Q8 10.8.

En olası açıklama dört bitlik ağırlıkların kullanılmadan önce açılması gereken ek iş. Prefill hesaba bağlı olduğu için bu maliyet orada kendini gösteriyor, decode bellek trafiğine bağlı olduğu için orada küçük dosya kazanıyor. Bunu bir hipotez olarak yazıyorum, çünkü izole eden bir deney kurmadım.

Sonuç olarak “küçük niceleme her zaman hızlıdır” cümlesi yanlış. Hangi fazdan bahsettiğinize bağlı.

Ölçerken düştüğüm tuzaklar

Bunların hepsi bu yazıyı hazırlarken başıma geldi.

Aynı promptu tekrar göndermek. Sunucu bir önceki isteğin önbelleğini kullanıp prefill’i tamamen atlıyor. Aynı promptta ilk token 98 milisaniyede geldi. Promptun başına rastgele bir etiket koyunca 599 milisaniye. Altı kat şişme. Dürüst sayı ikincisi.

Derlemenin ne ürettiğine bakmamak. İlk Android derlemem cihazın desteklediği komutları kullanmıyordu, çünkü bir cmake testi sessizce başarısız olmuştu. Aynı cihaz, aynı model ve aynı dört iş parçacığıyla prefill 22.4 tok/s okudu. Doğru bayraklarla 92.6 tok/s okudu, yani 4.1 kat.

Akıl yürütme tokenlarını kaçırmak. Modeller düşünme tokenlarını farklı alanlarda yolluyor, Qwen bir alanda, gpt-oss başka bir alanda. Sadece normal metin alanına bakarsanız ilk token geç görünür ve decode imkânsız çıkar. Geliştirme sırasında saniyede on binlerce token gösteren koşular gördüm, onlar ölçüm değil tamponlanmış akıştı. Yayınladığım sonuç dosyaları düzeltilmiş koddan geliyor, o bozuk koşular repoda yok.

Birkaç tokenlık çıktıdan decode hızı hesaplamak. Yirmi dört tokenın altında o sayı gürültüden ibaret. Bu yüzden sınıflandırma satırının decode hücresi bu yazıda boş, o koşular sadece beş altı token üretti.

Peki panoyu doğrulama işi

Başa dönelim. Teknisyen fotoğrafı çekiyor, uygulama panoyu doğruluyor.

Önce sınırı söyleyeyim. Bu yazıdaki bütün ölçümler metin girdisiyle. Fotoğraf girdisini ölçmedim, o yüzden aşağıdaki sayılar panoyu doğrulama işinin kendisi değil, ona en yakın metin işidir.

Bu iş kısa prompt ve çok kısa çıktı demek, yani beklemeyi belirleyen şey prefill. Ölçtüğüm 256 tokenlık promptta ilk cevap TPU yolunda 0.18 saniyede, CPU yolunda 1.30 saniyede geldi.

Ama bu sayıyı olduğu gibi kendi işinize taşımayın. O rakamların içinde 50 milisaniye civarında sabit bir gider var. Gerçekten kısa bir promptta, mesela 51 token, aynı sayılardan hesapladığım süre CPU’da 0.30 saniye ve TPU’da 0.08 saniye civarına iniyor. Yani oran on kat değil, dörde yakın. Hesap kabaca şöyle: 256 tokenlık koşudan sabit gideri çıkarıp kalan süreyi token başına ölçekledim.

Ama modeli her seferinde yeniden yüklerseniz, TPU yolunda üç saniyelik hazırlık her fotoğrafta tekrar ödeniyor. O zaman kazanç kayboluyor. Yani mimari kararı basit. Model sıcak kalacak, oturum açık tutulacak.

Fotoğraf girdisinde mekanizma aynı kalıyor ama ağırlık daha da prefill tarafına kayıyor, çünkü bir görüntü yüzlerce token olarak kodlanıyor. O tarafı ölçmeden bir sayı yazmayacağım. Bir sonraki yazının konusu bu olacak. Bir elektrik panosu fotoğrafı kaç token eder.

Koşu künyesi

Yazıdaki her sayının nereden geldiği. Bir satırı kendiniz tekrarlamak isterseniz gereken her şey burada.

SatırCihazÇalışma zamanıModel ve dosyaAyarİstatistik
iPhone üç işiPhone 15 Pro, iOS 26.6.2MLX Swiftmlx-community/Qwen3-1.7B-4bitdüşünme kapalı, 51 ile 1692 token arası prompt3 koşu medyanı, ısınma atıldı
Pixel CPUPixel 10 Pro XL, Android 17llama.cpp, CPUQwen3-1.7B-Q4_K_M.gguf, 1.03 GB4 ve 6 iş parçacığı, büyük çekirdeklere sabit, pp512 ve tg128araç içi 3 tekrar ortalaması
Pixel GPUPixel 10 Pro XLllama.cpp, Vulkanaynı dosyatam yüklemearaç içi 2 ile 3 tekrar
Pixel TPU ve CPUPixel 10 Pro XLAI Edge Gallery 1.0.19, LiteRT-LMGemma 4 E2B, TPU dosyası 4.0 GB, CPU dosyası 2.6 GB256 prompt, 256 üretimuygulamanın kendi 3 koşu ortalaması
MasaüstüMacBook Pro M3 Max, 48 GBllama.cpp, Metalaynı Qwen3 dosyaları, Q4_K_M ve Q8_0pp512 ve tg128araç içi 3 tekrar ortalaması

Yayılım da elimde. Örnek olarak Pixel’de 6 iş parçacığında decode 15.66 ± 2.17, 4 iş parçacığında 17.43 ± 0.74. Yani o iki sayı arasındaki fark, sapmaların içinde kalıyor ve kesin bir sıralama iddia etmiyorum. Ham çıktıların tamamı depoda.

Akılda kalacak tek şey

Model seçerken iki sayıya bakın.

Uzun metin okutuyorsanız prefill sizin bekleme sürenizdir, ve ölçtüğüm telefonda hızlandırıcı seçimi orada büyüklük mertebesinde fark yarattı. Uzun cevap yazdırıyorsanız decode sizin kullanıcı deneyiminizdir, ve aynı telefonda hızlandırıcı orada hiçbir şey kazandırmadı. Kazandıran şey token başına okunan baytı düşürmekti.

Bunu tek bir telefonda, tek bir modelde ölçtüm. Sizin cihazınızda sayılar farklı çıkacak. Değişmeyecek olan şey, bakmanız gereken iki ayrı sayı olduğu.

On iki yıl önce cevaplayamadığım soru buydu. Bugün cevabı ölçülebiliyor.

Okumak yerine izlemeyi tercih ederseniz bunun tamamını canlı yapabilirim. Ölçüm düzeneğini kurmak, gerçek telefona atmak, sayıları çıktığı anda okumak ve sonucu bir uygulamaya bağlamak. Söyleyin, kaydedeyim.