Baekpica commited on
Commit
55e73a6
·
verified ·
1 Parent(s): 0e613a2

model card: flash-decode depth numbers, rising concurrency, re-measured MTP; pin d35f0dd

Browse files
Files changed (1) hide show
  1. README.md +116 -99
README.md CHANGED
@@ -112,7 +112,7 @@ Composition: `calibration.composition.json`.
112
  | BF16 GGUF sha256 | `73be2da8653976df036bf9b6466b011f86cb10f78bab30a47025638ec999d3f8` |
113
  | llama.cpp (quantizer) | [`ggml-org/llama.cpp`](https://github.com/ggml-org/llama.cpp) @ `6a32c29a746a2e44de463de647f9f6661eb5086b` (build `b10295`) |
114
  | Converter | [`Baekpica/k-exaone-mixed-ds4`](https://github.com/Baekpica/k-exaone-mixed-ds4) |
115
- | Serving engine (measured below) | [`Baekpica/ds4`](https://github.com/Baekpica/ds4/tree/feature/exaone-model-loader) @ `920427ac124078af021a0736792d2115b1d00bc2` |
116
  | — upstream engine | [`antirez/ds4`](https://github.com/antirez/ds4) |
117
  | — DGX Spark port | [`Entrpi/ds4-on-spark`](https://github.com/Entrpi/ds4-on-spark) |
118
 
@@ -202,7 +202,7 @@ driver 595.71.05, CUDA 13.3, Linux 6.17.
202
  |---|---|
203
  | Engine | [`Baekpica/ds4`](https://github.com/Baekpica/ds4/tree/feature/exaone-model-loader) |
204
  | Branch | `feature/exaone-model-loader` |
205
- | Commit | `920427ac124078af021a0736792d2115b1d00bc2` |
206
  | Weights | [`Baekpica/K-EXAONE-236B-A23B-Mixed-Quant-GGUF`](https://huggingface.co/Baekpica/K-EXAONE-236B-A23B-Mixed-Quant-GGUF), variant **v1** |
207
  | Converter / reports | [`Baekpica/k-exaone-mixed-ds4`](https://github.com/Baekpica/k-exaone-mixed-ds4) |
208
 
@@ -238,7 +238,7 @@ the single most common way to get wrong kernel results here.
238
  ```bash
239
  git clone https://github.com/Baekpica/ds4
240
  cd ds4
241
- git checkout 920427ac124078af021a0736792d2115b1d00bc2
242
  make cuda-spark
243
  ```
244
 
@@ -352,42 +352,48 @@ Raw per-request records ship in the converter repository.
352
 
353
  | Prompt tokens | Prefill t/s | Decode t/s | Time to first token |
354
  |---:|---:|---:|---:|
355
- | 1 451 | 53.0 | 10.51 | 27.4 s |
356
- | 3 941 | 51.6 | 9.05 | 76.4 s |
357
- | 8 222 | 47.9 | 7.38 | 171.6 s |
358
- | 16 376 | 42.3 | 5.42 | 387.1 s |
359
 
360
- Both curves are clean enough to fit and extrapolate.
 
 
 
 
361
 
362
- **Decode cost is linear in context depth:**
363
 
364
  ```text
365
- ms per token = 86.6 + 0.00597 × context_tokens (residuals < 0.4 ms)
366
  ```
367
 
368
- **Prefill cost is quadratic in prompt length**, because the marginal cost of the
369
- next 2048 tokens grows linearly with the depth they start at:
370
 
371
  ```text
372
- seconds per 2048-token chunk = 37.5 + 0.00145 × depth_tokens (16 points, residuals < 0.25 s)
373
  ```
374
 
375
- Which gives, for depths beyond what was measured directly:
376
 
377
- | Context | Marginal prefill t/s | Cold prefill of a full prompt | Decode t/s |
378
- |---:|---:|---:|---:|
379
- | 8 192 | 41.5 | 2.7 min | 7.4 (measured) |
380
- | 32 768 | 24.1 | 16 min | 3.5 |
381
- | 65 536 | 15.5 | 45 min | 2.1 |
382
- | 131 072 | 9.0 | 2.3 h | 1.2 |
383
- | 262 144 | 4.9 | 8.0 h | 0.6 |
384
 
385
  ### What that means in practice
386
 
387
- The 262 144-token context **fits, is allocated, and is resident** that is a
388
- memory result and it holds. It is not a throughput result. A cold 256K prompt
389
- would take hours to prefill on this hardware, and decode at that depth runs
390
- below 1 token/s. **Useful working depths on one GB10 today are roughly 2K–32K.**
 
 
 
391
 
392
  **Multi-turn chat reuses the prefix; a cold prompt does not.** A continuation
393
  resumes at the point where it diverges from what the session already holds, so
@@ -415,41 +421,52 @@ than a tunable: the ring is `window + prefill chunk` wide, so a divergence
415
  further back than about 2 000 tokens falls back to a cold prefill. Typical chat
416
  divergence is one assistant turn, well inside it. Requires ds4 at the commit
417
  pinned above.
418
- Concurrency, however, does **not** help today. With `--batched-session 8` and
419
- short prompts so prefill cannot interfere, aggregate decode throughput is flat:
 
 
 
420
 
421
- | Concurrent streams | Summed decode t/s | Per stream |
422
- |---:|---:|---:|
423
- | 1 | 11.12 | 11.12 |
424
- | 2 | 9.80 | ~4.9 |
425
- | 4 | 10.03 | ~2.5 |
426
- | 8 | 10.80 | ~1.35 |
427
-
428
- Some of that is inherent in a top-8-of-128 MoE, concurrent tokens route to
429
- largely disjoint experts, so routed-expert weight reads do not amortise across
430
- a batch. The shared components should still amortise and do not appear to.
431
- With **cold** prompts it is worse than flat: prefill is serialised across slots,
432
- so `N` concurrent long requests behave like `N` sequential ones and wall
433
- throughput *falls* (3.18 t/s at one stream to 2.01 t/s at eight).
434
-
435
- Batching itself is free at concurrency 1 the batched server matches the plain
436
- one (10.39 vs 10.51 t/s at 2K, 7.37 vs 7.38 at 8K) — so `--batched-session` is
437
- worth using for fairness and slot residency, just not for throughput.
 
 
 
 
 
 
438
 
439
  ### Where the time goes
440
 
441
  The 12 full-attention layers hold **49 152 bytes of KV per context position**
442
- (GQA, 8 KV heads × 128 dims, K and V, f16). Decode adds **5.97 µs per context
443
- position**, which is an effective **8.2 GB/s** on a part with roughly 273 GB/s
444
- of memory bandwidth about **3 %**. The depth-dependent part of decode is
445
- therefore not bandwidth-bound. The `exaone-moe` GQA decode attention path is the
446
- limiter, and it is the obvious first optimisation target.
447
-
448
- For scale: ds4's tuned MLA path on DeepSeek V4 Flash reaches 825 t/s prefill and
449
- 18 t/s decode on this same GB10 (`speed-bench/gb10.csv` in the engine repo).
450
- That model has far fewer active parameters, so the absolute numbers are not
451
- comparable but the *shape* is. MLA prefill is nearly flat with depth
452
- (825 823 t/s from 2K to 64K) where `exaone-moe` roughly halves every 4×.
 
 
453
 
454
  ### OpenAI-compatible API
455
 
@@ -496,39 +513,34 @@ model, no second weight copy. It is **opt-in and off by default**:
496
  speculation for the rest of the session when measured MTP work runs more than
497
  3 % slower. `DS4_EXAONE_MTP_NO_QUENCH=1` defeats it, for measurement only.
498
 
499
- **It loses at short context and reaches break-even at long context.** Measured
500
- with the quench defeated, so the whole generation is speculative:
501
-
502
- | Context | Draft acceptance | Cycle | MTP ms/token | Plain ms/token | vs plain |
503
- |---:|---:|---:|---:|---:|---:|
504
- | 1 451 | 69.3 % | 204.1 ms | 120.5 | 95.6 | +26 % |
505
- | 7 924 | 44.3 % | 241.7 ms | 167.5 | 133.7 | +25 % |
506
- | 32 995 | 34.0 % | 390.3 ms | 291.2 | 281.2 | **+4 %** |
507
-
508
- The mechanism is a single ratio. A cycle runs one draft pass (~13 ms, roughly
509
- constant) plus one **two-row** target verify pass. Write **k** for the cost of
510
- that two-row pass relative to an ordinary one-row decode, and **a** for draft
511
- acceptance; a cycle commits `1 + a` tokens, so speculation wins exactly when
512
 
513
- ```text
514
- k < 1 + a
515
- ```
516
-
517
- Measured, k falls with depth 2.06, 1.76, **1.36** because the KV read the
518
- two rows share amortises as the context grows, while acceptance falls — 69.3 %,
519
- 44.3 %, 34.0 % because the MTP block's private KV ring is only 128 rows and
520
- starts cold. At 32 995 tokens the two sides are 1.36 against 1.340: break-even
521
- to within measurement noise, and still improving with depth.
522
-
523
- So `k` is the lever, not draft quality. An ideal two-row pass would share the
524
- weight streaming between its rows and cost `k 1.05`, at which point even 34 %
525
- acceptance turns into roughly a 25 % speedup. That k is 1.36 rather than 1.05
526
- is the same finding as the decode-attention result above: this path is bound by
527
- per-row cost, not by bandwidth.
528
-
529
- **Keep MTP off for serving today** it is a loss below ~32K and a wash above.
530
- It becomes worth enabling if either the two-row verify pass gets cheaper or the
531
- MTP KV is warmed from the prompt instead of starting cold.
 
 
 
 
532
 
533
  ## Measured quality
534
 
@@ -568,23 +580,28 @@ reference's own scores.
568
  - Evaluation is a 32-prompt fixture set plus the token-fidelity comparison
569
  above, not a full benchmark suite. Raw results, including the failures, ship
570
  in the converter repository.
571
- - **256K is a memory result, not a throughput result.** The context is allocated
572
- and resident, but a cold prompt at that depth takes hours to prefill and
573
- decodes below 1 token/s. Plan for 2K–32K of working context on one GB10.
574
- - **Prefill is quadratic and decode is linear in context depth**, both steeper
575
- than ds4's MLA models on the same hardware. The headroom is specifically in
576
- the *depth-dependent* half: the decode attention path reads KV at only ~3 % of
577
- the device's memory bandwidth. The depth-independent half streaming the
578
- active weights is already near the roofline, so this is not a claim that
579
- decode as a whole is 30× off.
580
  - **The MTP block only runs under ds4**, on the pinned branch and commit above.
581
  Under llama.cpp it is inert. There is no third runtime that executes it.
582
- - **MTP is a loss below ~32K of context and a wash above it**, so it ships off
583
- by default and auto-quenches when enabled. The limit is the cost of the
584
- two-row verify pass, not draft quality.
585
  - **MTP does not run under `--batched-session`.** ds4 disables speculative
586
  decoding whenever native session batching is active, so concurrency > 1 is
587
  plain decode regardless of the MTP flags.
 
 
 
 
 
588
  - **Multi-turn reuse reaches back about 2 000 tokens.** The sliding-window KV
589
  ring is `window + prefill chunk` wide, and a resumed prefill needs the window
590
  that preceded its restart point. A conversation that diverges further back
 
112
  | BF16 GGUF sha256 | `73be2da8653976df036bf9b6466b011f86cb10f78bab30a47025638ec999d3f8` |
113
  | llama.cpp (quantizer) | [`ggml-org/llama.cpp`](https://github.com/ggml-org/llama.cpp) @ `6a32c29a746a2e44de463de647f9f6661eb5086b` (build `b10295`) |
114
  | Converter | [`Baekpica/k-exaone-mixed-ds4`](https://github.com/Baekpica/k-exaone-mixed-ds4) |
115
+ | Serving engine (measured below) | [`Baekpica/ds4`](https://github.com/Baekpica/ds4/tree/feature/exaone-model-loader) @ `d35f0dd60af73c22dbd056fdad2eb616781fa6bd` |
116
  | — upstream engine | [`antirez/ds4`](https://github.com/antirez/ds4) |
117
  | — DGX Spark port | [`Entrpi/ds4-on-spark`](https://github.com/Entrpi/ds4-on-spark) |
118
 
 
202
  |---|---|
203
  | Engine | [`Baekpica/ds4`](https://github.com/Baekpica/ds4/tree/feature/exaone-model-loader) |
204
  | Branch | `feature/exaone-model-loader` |
205
+ | Commit | `d35f0dd60af73c22dbd056fdad2eb616781fa6bd` |
206
  | Weights | [`Baekpica/K-EXAONE-236B-A23B-Mixed-Quant-GGUF`](https://huggingface.co/Baekpica/K-EXAONE-236B-A23B-Mixed-Quant-GGUF), variant **v1** |
207
  | Converter / reports | [`Baekpica/k-exaone-mixed-ds4`](https://github.com/Baekpica/k-exaone-mixed-ds4) |
208
 
 
238
  ```bash
239
  git clone https://github.com/Baekpica/ds4
240
  cd ds4
241
+ git checkout d35f0dd60af73c22dbd056fdad2eb616781fa6bd
242
  make cuda-spark
243
  ```
244
 
 
352
 
353
  | Prompt tokens | Prefill t/s | Decode t/s | Time to first token |
354
  |---:|---:|---:|---:|
355
+ | 697 | 51.8 | 10.87 | 13.5 s |
356
+ | 1 896 | 52.8 | 10.17 | 35.9 s |
357
+ | 8 087 | 47.3 | **10.03** | 171.0 s |
358
+ | 33 343 | 32.2 | **8.73** | 1 035 s |
359
 
360
+ Decode barely falls with depth any more. The engine's flash-decode split
361
+ (2026-08-08) rebuilt the deep-context decode path: the depth term dropped from
362
+ 5.88 to 0.61 µs per context position, so a 32K-deep session decodes at 8.7 t/s
363
+ where it managed 3.5 before. An earlier version of this card said the decode
364
+ attention path ran at ~3 % of memory bandwidth; that headroom is now spent.
365
 
366
+ **Decode cost is linear in context depth, and nearly flat:**
367
 
368
  ```text
369
+ ms per token = 94.5 + 0.00061 × context_tokens (4 cells, residuals < 3 ms)
370
  ```
371
 
372
+ **Prefill cost is quadratic in prompt length** and is now the one wall left:
 
373
 
374
  ```text
375
+ TTFT seconds = 0.0180 × N + 3.92e-7 × (residuals < 0.7 s)
376
  ```
377
 
378
+ Extrapolating decode beyond the measured 33K (prefill from the same fit):
379
 
380
+ | Context | Cold prefill of a full prompt | Decode t/s |
381
+ |---:|---:|---:|
382
+ | 8 192 | 2.6 min (measured) | 10.0 (measured) |
383
+ | 32 768 | 17 min (measured) | 8.7 (measured) |
384
+ | 65 536 | 47 min | 7.4 |
385
+ | 131 072 | 2.3 h | 5.7 |
386
+ | 262 144 | 8.8 h | 3.9 |
387
 
388
  ### What that means in practice
389
 
390
+ The 262 144-token context **fits, is allocated, and is resident**, and decode
391
+ now stays useful an order of magnitude deeper than it used to 8.7 t/s
392
+ measured at 33K, ~5.7 t/s extrapolated at 128K. What has not moved is the cost
393
+ of getting there cold: prefill is quadratic, a 33K prompt takes 17 minutes and
394
+ a full 256K one would take ~9 hours. **The working-depth limit on one GB10 is
395
+ now set by how long a cold prefill you will tolerate — not by decode.**
396
+ Warm continuations skip it (below).
397
 
398
  **Multi-turn chat reuses the prefix; a cold prompt does not.** A continuation
399
  resumes at the point where it diverges from what the session already holds, so
 
421
  further back than about 2 000 tokens falls back to a cold prefill. Typical chat
422
  divergence is one assistant turn, well inside it. Requires ds4 at the commit
423
  pinned above.
424
+ **Concurrency now helps.** ds4's cross-session row batching (2026-08-08) runs
425
+ concurrent decode steps through one pass the weight-bound stages are read
426
+ once for all streams — so aggregate throughput rises with load instead of
427
+ staying flat. Steady-state aggregate decode, `--batched-session 8`, short
428
+ prompts, all streams decoding:
429
 
430
+ | Concurrent streams | Summed decode t/s | Per stream | before row batching |
431
+ |---:|---:|---:|---:|
432
+ | 1 | 11.5 | 11.5 | 11.1 |
433
+ | 2 | 14.8 | ~7.4 | 9.8 |
434
+ | 4 | 16.3 | ~4.1 | 10.0 |
435
+ | 8 | **18.5** | ~2.3 | 10.8 |
436
+
437
+ An operator serving several users sees ~15–18 tok/s of total output; a single
438
+ user still sees the single-stream rate above. The remaining per-row floor is
439
+ mostly the routed experts concurrent tokens route to largely disjoint
440
+ top-8-of-128 sets, so that read genuinely cannot amortise plus the per-row
441
+ attention, which is per-session by construction.
442
+
443
+ A prefill no longer blocks the batch either: a pending prefill quantum rides
444
+ the decode batch's weight sweep (`+prefill` in the batch log), so admitting a
445
+ new long prompt costs the running streams far less than alternating whole
446
+ passes did.
447
+
448
+ One contract changed with row batching: **greedy output across batch widths is
449
+ not bit-stable at near-ties**. A request decoded alongside seven others can
450
+ pick a different token than the same request alone where the top-2 margin is
451
+ tiny, deterministically per batch composition. Sequential (width-1) decode is
452
+ unchanged, and the same batch always reproduces the same output.
453
 
454
  ### Where the time goes
455
 
456
  The 12 full-attention layers hold **49 152 bytes of KV per context position**
457
+ (GQA, 8 KV heads × 128 dims, K and V, f16). Decode adds **0.61 µs per context
458
+ position** an effective ~81 GB/s of KV read against roughly 273 GB/s of
459
+ device bandwidth. An earlier engine paid 5.97 µs here (~3 % of bandwidth,
460
+ one attention block per head); the flash-decode split closed most of that,
461
+ and what remains splits between the depth-independent floor (~94 ms/token,
462
+ streaming the active weights, near the roofline) and the last ~3× of the
463
+ attention read.
464
+
465
+ Prefill keeps the one-block-per-(token, head) attention kernel and its
466
+ quadratic term is now the dominant cost of deep contexts. For scale: ds4's
467
+ tuned MLA path on DeepSeek V4 Flash reaches 825 t/s prefill on this same GB10
468
+ and is nearly flat with depth, where `exaone-moe` prefill roughly halves every
469
+ 4× — that gap is the open kernel problem, not decode.
470
 
471
  ### OpenAI-compatible API
472
 
 
513
  speculation for the rest of the session when measured MTP work runs more than
514
  3 % slower. `DS4_EXAONE_MTP_NO_QUENCH=1` defeats it, for measurement only.
515
 
516
+ **It is close to a wash now, and still not a win.** Measured with the quench
517
+ defeated so the whole generation is speculative, on the engine's current
518
+ kernels (the two-row verify rides the same aligned-dispatch, small-batch and
519
+ flash-decode tiers as everything else):
 
 
 
 
 
 
 
 
 
520
 
521
+ | Context | Draft acceptance | MTP ms/token | Plain ms/token | vs plain |
522
+ |---:|---:|---:|---:|---:|
523
+ | 1 387 | 36.6 % | 108.8 | 98.3 | +11 % |
524
+ | 7 752 | 60.8 % | 102.5 | 99.7 | **+2.8 %** |
525
+ | 33 914 | 37.0 % | 142.8 | 114.5 | +25 % |
526
+
527
+ The mechanism is a single ratio: a cycle runs one draft pass plus one
528
+ **two-row** target verify pass; with **k** the verify's cost relative to a
529
+ one-row decode and **a** the acceptance, a cycle commits `1 + a` tokens and
530
+ wins exactly when `k < 1 + a`. An earlier engine paid k ≈ 2 at shallow depth
531
+ because its two-row pass re-read the weights per row; that k is now near its
532
+ floor, which moved MTP from a 26–50 % loss to the table above. What remains
533
+ is acceptance: at the 3 % quench threshold the 8K row is already a wash, and
534
+ five to ten more points of acceptance a warmed MTP ring instead of a cold
535
+ 128-row one, or corpus luck — is the difference between off and on.
536
+
537
+ Acceptance numbers are a property of the text (they moved 20 points between
538
+ corpus slices in these very measurements); compare k across runs, not
539
+ acceptance.
540
+
541
+ **MTP stays off by default.** The auto-quench makes `--exaone-mtp` safe to
542
+ try on workloads where drafts land often; nothing here changes greedy output
543
+ either way.
544
 
545
  ## Measured quality
546
 
 
580
  - Evaluation is a 32-prompt fixture set plus the token-fidelity comparison
581
  above, not a full benchmark suite. Raw results, including the failures, ship
582
  in the converter repository.
583
+ - **256K remains gated by cold prefill, not decode.** Decode now holds 8.7 t/s
584
+ at a measured 33K and ~4 t/s extrapolated at the full context, but a cold
585
+ 256K prefill still takes ~9 hours. Deep contexts are practical exactly when
586
+ they are reached warm through prefix reuse rather than cold.
587
+ - **Prefill is quadratic in context depth and is the one wall left.** The
588
+ decode-side headroom an earlier version of this card described is spent: the
589
+ flash-decode split brought the depth term from 5.88 to 0.61 µs per position.
590
+ The prefill attention kernel keeps the exact one-block form; a tiled variant
591
+ measured a wash and a tensor-core revision is the known next step.
592
  - **The MTP block only runs under ds4**, on the pinned branch and commit above.
593
  Under llama.cpp it is inert. There is no third runtime that executes it.
594
+ - **MTP is a mild loss (3–25 % by depth and corpus)**, so it ships off by
595
+ default and auto-quenches when enabled. Its verify cost is now near its
596
+ floor; the remaining limit is draft acceptance.
597
  - **MTP does not run under `--batched-session`.** ds4 disables speculative
598
  decoding whenever native session batching is active, so concurrency > 1 is
599
  plain decode regardless of the MTP flags.
600
+ - **Greedy output across batch widths is not bit-stable at near-ties.** A
601
+ request decoded alongside others can pick a different token than the same
602
+ request alone where the top-2 margin is tiny; the same batch composition
603
+ always reproduces the same output, and width-1 decode is unchanged. This is
604
+ the standard batched-inference contract.
605
  - **Multi-turn reuse reaches back about 2 000 tokens.** The sliding-window KV
606
  ring is `window + prefill chunk` wide, and a resumed prefill needs the window
607
  that preceded its restart point. A conversation that diverges further back