Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 73 additions & 1 deletion internal/publish/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ import (
// it does not know by name rather than mis-indexing it quietly.
const IndexSchema = 1

// GPUMem is one card at the end of the run, the figures the card's MEMORY
// block prints for it (internal/card memorySection / vramLine). Index is the
// device's own, including 0. UsedBytes and VRAMBytes are bytes; 0 was not
// read and omitempty keeps it off the wire rather than printing a zero.
type GPUMem struct {
Index int `json:"index"`
UsedBytes int64 `json:"used_bytes,omitempty"`
VRAMBytes int64 `json:"vram_bytes,omitempty"`
}

// Index is what the search reads: one row per published run. It is derived
// from a tape by IndexOf and uploaded beside it.
type Index struct {
Expand Down Expand Up @@ -66,6 +76,18 @@ type Index struct {
RAMBytes int64 `json:"ram_bytes,omitempty"`
HostClass string `json:"host_class,omitempty"`

// Memory the card's MEMORY block prints, copied from the tape the way
// memorySection reads it. gpu_mem is one entry per GPUsAtEnd sample,
// omitted on a unified-memory host (the card prints "Unified …" there,
// not a per-GPU line) and when no sample was taken. VRAMBytes on an
// entry is that card's capacity from Host.GPUs, by index, and 0 stays
// absent. The RSS fields are Memory.AtEnd; a zero RSS is no /proc view,
// which the card prints as "?" — so it is absent here, never a 0.
GPUMem []GPUMem `json:"gpu_mem,omitempty"`
RSSBytes int64 `json:"rss_bytes,omitempty"`
RSSFileBytes int64 `json:"rss_file_bytes,omitempty"`
RSSAnonBytes int64 `json:"rss_anon_bytes,omitempty"`

// What it did. Mode is the summary's own (tape.ModeChat,
// tape.ModeDecision), absent for a benchmark (TTP-187): a reader that
// shows a rate must know first whether the row has one.
Expand All @@ -74,7 +96,18 @@ type Index struct {
PromptSet string `json:"prompt_set,omitempty"`
DecodePerSec float64 `json:"decode_per_sec,omitempty"`
PrefillPerSec float64 `json:"prefill_per_sec,omitempty"`
TTFTp50Ms float64 `json:"ttft_p50_ms,omitempty"`
// ShortPrompt is card.ShortPrompt: the prompt was too short for
// PrefillPerSec to be a prefill measurement. Omitted otherwise. When
// it is set, PrefillPerSec is left unset so the search column stays
// NULL — the tape still holds the server's figure.
ShortPrompt bool `json:"short_prompt,omitempty"`
// The probe pass's own figures, present on the same gate as the card's
// probeLine (Probe.PrefillPerSecond > 0). Pointers so a measured fixed
// cost of 0 ms stays on the wire: formatProbeMs treats 0 as a reading,
// and omitempty on a float would drop it. index_json only — no D1 column.
ProbePrefillPerSec *float64 `json:"probe_prefill_per_sec,omitempty"`
ProbeFixedMs *float64 `json:"probe_fixed_ms,omitempty"`
TTFTp50Ms float64 `json:"ttft_p50_ms,omitempty"`

// The figures behind a rate (TTP-130). Every field is additive and
// omitempty: absent means unknown, and the reader prints ? or omits
Expand Down Expand Up @@ -207,6 +240,28 @@ func IndexOf(t *tape.Tape) Index {
}
}

// The same two gates memorySection uses before it draws a per-GPU line:
// a unified host has one pool, and an empty end-of-run sample is "VRAM
// ?", not a row of zeros. Capacity is the host card's, looked up by
// index — the sample's position is not the card's.
if tape.Unified(s.Host) == "" && len(s.GPUsAtEnd) > 0 {
total := map[int]int64{}
for _, g := range s.Host.GPUs {
total[g.Index] = g.VRAMBytes
}
idx.GPUMem = make([]GPUMem, len(s.GPUsAtEnd))
for i, g := range s.GPUsAtEnd {
idx.GPUMem[i] = GPUMem{Index: g.Index, UsedBytes: g.UsedBytes, VRAMBytes: total[g.Index]}
}
}
// Host RSS only when a /proc view was taken. File and anon ride along;
// a zero of either is omitempty, the card's "?".
if m := s.Memory.AtEnd; m.RSSBytes > 0 {
idx.RSSBytes = m.RSSBytes
idx.RSSFileBytes = m.RSSFileBytes
idx.RSSAnonBytes = m.RSSAnonBytes
}

idx.Mode = s.Mode
if d := s.Decision; s.IsDecision() && d != nil {
// No stream count: the burst lanes are requests in flight, and a
Expand Down Expand Up @@ -237,6 +292,23 @@ func IndexOf(t *tape.Tape) Index {
idx.PrefillPerSec = s.Timings.PromptPerSecond
idx.TTFTp50Ms = s.Timings.TTFTMs
}
// Decision rows are not part of this: a decision has no prefill
// measurement to qualify, and its row is left exactly as the block
// above wrote it (internal/tape.IsDecision).
if !s.IsDecision() {
// card.ShortPrompt, not a second copy of the threshold.
if card.ShortPrompt(&s) {
idx.ShortPrompt = true
idx.PrefillPerSec = 0
}
// probeLine's gate. A refused fit and a missing probe are each
// "not observed" and travel as nothing.
if p := s.Probe; p != nil && p.PrefillPerSecond > 0 {
rate, fixed := p.PrefillPerSecond, p.FixedMs
idx.ProbePrefillPerSec = &rate
idx.ProbeFixedMs = &fixed
}
}

// The figures behind a rate (TTP-130): the card's "in" figure, its
// "out" figure, and everything that qualifies them. MinPredictedN lives
Expand Down
132 changes: 132 additions & 0 deletions internal/publish/index_short_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package publish

import (
"encoding/json"
"testing"

"github.com/midagedev/toktape/internal/tape"
)

// TestIndexOfShortPrompt is the search contract for a prompt under
// tape.MinPrefillPromptTokens (card.ShortPrompt): the server's prefill rate
// stays on the tape and leaves the index, because it is not a prefill
// measurement. The probe's own figures travel instead, on probeLine's gate
// (PrefillPerSecond > 0). Assertions are on the marshalled keys.
func TestIndexOfShortPrompt(t *testing.T) {
probe := &tape.ProbeSummary{PrefillPerSecond: 679, FixedMs: 134}

t.Run("33 tokens with a probe", func(t *testing.T) {
m := indexKeys(t, oneStream(33, 206, probe))
if m["short_prompt"] != true {
t.Errorf("short_prompt = %v, want true", m["short_prompt"])
}
if _, ok := m["prefill_per_sec"]; ok {
t.Errorf("prefill_per_sec = %v, want the key absent", m["prefill_per_sec"])
}
if m["probe_prefill_per_sec"] != float64(679) {
t.Errorf("probe_prefill_per_sec = %v, want 679", m["probe_prefill_per_sec"])
}
if m["probe_fixed_ms"] != float64(134) {
t.Errorf("probe_fixed_ms = %v, want 134", m["probe_fixed_ms"])
}
})

t.Run("5673 tokens", func(t *testing.T) {
m := indexKeys(t, oneStream(5673, 206, probe))
if _, ok := m["short_prompt"]; ok {
t.Errorf("short_prompt = %v, want the key absent", m["short_prompt"])
}
if m["prefill_per_sec"] != float64(206) {
t.Errorf("prefill_per_sec = %v, want 206", m["prefill_per_sec"])
}
// The probe is a fact about the machine, not about this prompt.
if m["probe_prefill_per_sec"] != float64(679) || m["probe_fixed_ms"] != float64(134) {
t.Errorf("probe fields = %v / %v, want 679 / 134", m["probe_prefill_per_sec"], m["probe_fixed_ms"])
}
})

t.Run("short with no probe", func(t *testing.T) {
m := indexKeys(t, oneStream(33, 206, nil))
if m["short_prompt"] != true {
t.Errorf("short_prompt = %v, want true", m["short_prompt"])
}
if _, ok := m["prefill_per_sec"]; ok {
t.Errorf("prefill_per_sec = %v, want the key absent", m["prefill_per_sec"])
}
for _, key := range []string{"probe_prefill_per_sec", "probe_fixed_ms"} {
if _, ok := m[key]; ok {
t.Errorf("key %q present on a tape with no probe: %v", key, m[key])
}
}
})

// A measured intercept of 0 ms is a reading (formatProbeMs), not an
// unknown, so the key stays on the wire.
t.Run("probe fixed cost of 0", func(t *testing.T) {
m := indexKeys(t, oneStream(5673, 206, &tape.ProbeSummary{PrefillPerSecond: 679, FixedMs: 0}))
if m["probe_fixed_ms"] != float64(0) {
t.Errorf("probe_fixed_ms = %v, want 0 present", m["probe_fixed_ms"])
}
if _, ok := m["probe_fixed_ms"]; !ok {
t.Error("probe_fixed_ms key absent; 0 ms is a reading")
}
})

// A refused fit (rate 0, points kept) is the same "not observed" as no
// probe: probeLine prints nothing.
t.Run("refused fit", func(t *testing.T) {
m := indexKeys(t, oneStream(33, 206, &tape.ProbeSummary{PrefillPerSecond: 0, FixedMs: 0}))
for _, key := range []string{"probe_prefill_per_sec", "probe_fixed_ms"} {
if _, ok := m[key]; ok {
t.Errorf("key %q present on a refused fit: %v", key, m[key])
}
}
})

// Decision rows stay as they were: no short_prompt, no probe keys, and
// the prefill figure the old path copied is still copied.
t.Run("decision untouched", func(t *testing.T) {
s := oneStream(33, 206, probe)
s.Mode = tape.ModeDecision
m := indexKeys(t, s)
if _, ok := m["short_prompt"]; ok {
t.Errorf("short_prompt on a decision row: %v", m["short_prompt"])
}
for _, key := range []string{"probe_prefill_per_sec", "probe_fixed_ms"} {
if _, ok := m[key]; ok {
t.Errorf("key %q on a decision row: %v", key, m[key])
}
}
if m["prefill_per_sec"] != float64(206) {
t.Errorf("prefill_per_sec = %v, want the decision path left at 206", m["prefill_per_sec"])
}
})
}

// oneStream is a one-stream tape whose whole prompt is promptN tokens
// (Cache.PromptTotal, the count card.ShortPrompt asks through PromptTokens)
// and whose server prefill rate is rate. The rate is what must disappear
// from the index when the prompt is short.
func oneStream(promptN int, rate float64, probe *tape.ProbeSummary) tape.RunSummary {
return tape.RunSummary{
Concurrency: 1,
Cache: tape.CacheSummary{PromptTotal: promptN},
Timings: tape.TimingsSummary{PromptN: promptN, PromptPerSecond: rate},
Probe: probe,
}
}

// indexKeys is the marshalled index as a key set: an omitempty field that
// did not travel is not in the map.
func indexKeys(t *testing.T, s tape.RunSummary) map[string]any {
t.Helper()
b, err := json.Marshal(IndexOf(&tape.Tape{Summary: s}))
if err != nil {
t.Fatalf("marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(b, &m); err != nil {
t.Fatalf("unmarshal: %v", err)
}
return m
}
127 changes: 125 additions & 2 deletions internal/publish/index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ func TestIndexOfSingleStream(t *testing.T) {
RAMBytes: 68719476736,
HostClass: "2x rtx-3090",

// The card's MEMORY block. The want grew the fields the example
// tape already carries (GPUsAtEnd used bytes, Host.GPUs capacity,
// Memory.AtEnd); no assertion was loosened.
GPUMem: []GPUMem{
{Index: 0, UsedBytes: 25555748454, VRAMBytes: 25769803776},
{Index: 1, UsedBytes: 24481361100, VRAMBytes: 25769803776},
},
RSSBytes: 1288490189,
RSSFileBytes: 858993459,
RSSAnonBytes: 429496730,

Sessions: 1,
DecodePerSec: 17.4, // single stream: the stream's own rate
PrefillPerSec: 610,
Expand Down Expand Up @@ -188,6 +199,8 @@ func TestIndexOfZeroTape(t *testing.T) {
"batch", "ubatch", "ngl", "offload", "draft_model", "draft_accept",
"throttled", "cold", "power_w", "power_limit_w", "file_bytes",
"active_params", "n_experts", "n_experts_used",
"gpu_mem", "rss_bytes", "rss_file_bytes", "rss_anon_bytes",
"short_prompt", "probe_prefill_per_sec", "probe_fixed_ms",
} {
if bytes.Contains(b, []byte(`"`+key+`"`)) {
t.Errorf("unset field %q present in zero-tape JSON: %s", key, b)
Expand Down Expand Up @@ -285,8 +298,10 @@ func TestIndexJSONRoundTrip(t *testing.T) {
"model_dir", "params", "moe",
"quant_raw", "quant_id", "quant_bits", "engine_kind", "engine_version",
"os", "gpus_raw", "gpu_id", "gpu_ids", "gpu_count", "vram_bytes", "ram_bytes",
"host_class", "sessions", "prompt_set", "decode_per_sec",
"prefill_per_sec", "ttft_p50_ms", "caveats", "caveat_count",
"host_class", "gpu_mem", "rss_bytes", "rss_file_bytes", "rss_anon_bytes",
"sessions", "prompt_set", "decode_per_sec",
"prefill_per_sec", "short_prompt", "probe_prefill_per_sec", "probe_fixed_ms",
"ttft_p50_ms", "caveats", "caveat_count",
"prompt_n", "predicted_n", "min_predicted_n", "reasoning_n",
"cache_hit_ratio", "ctx_size", "n_slots", "fa", "kv_cache",
"batch", "ubatch", "ngl", "offload", "draft_model", "draft_accept",
Expand Down Expand Up @@ -724,3 +739,111 @@ func TestIndexOfNamesTheCardsTheServerUsed(t *testing.T) {
t.Errorf("unknown use: gpu_ids %v gpus_raw %v count %d, want both cards", idx.GPUIDs, idx.GPUsRaw, idx.GPUCount)
}
}

// TestIndexOfMemory pins the figures the card's MEMORY block prints
// (internal/card memorySection / vramLine). They are copied off the tape:
// one gpu_mem entry per GPUsAtEnd sample, vram from Host.GPUs by index and
// omitted at 0, RSS from Memory.AtEnd and omitted when RSSBytes is 0, and
// no gpu_mem on a unified host, where the card prints "Unified …" instead.
func TestIndexOfMemory(t *testing.T) {
t.Run("two gpus and rss", func(t *testing.T) {
// Host order is not sample order, and the second card's capacity
// was not read: vram is matched by index, and 0 stays off the wire.
s := tape.RunSummary{
Host: tape.HostInfo{GPUs: []tape.GPUInfo{
{Index: 1, Name: "NVIDIA GeForce RTX 3060", VRAMBytes: 0},
{Index: 0, Name: "NVIDIA GeForce RTX 3060", VRAMBytes: 12884901888},
}},
GPUsAtEnd: []tape.GPUSample{
{Index: 0, UsedBytes: 11164188672},
{Index: 1, UsedBytes: 8589934592},
},
Memory: tape.MemorySummary{AtEnd: tape.MemSample{
RSSBytes: 12456681472,
RSSFileBytes: 11947323392,
RSSAnonBytes: 263991296,
}},
}
idx := IndexOf(&tape.Tape{Summary: s})
want := []GPUMem{
{Index: 0, UsedBytes: 11164188672, VRAMBytes: 12884901888},
{Index: 1, UsedBytes: 8589934592},
}
if !reflect.DeepEqual(idx.GPUMem, want) {
t.Errorf("GPUMem = %+v, want %+v", idx.GPUMem, want)
}
if idx.RSSBytes != 12456681472 || idx.RSSFileBytes != 11947323392 || idx.RSSAnonBytes != 263991296 {
t.Errorf("RSS = %d / %d / %d, want 12456681472 / 11947323392 / 263991296",
idx.RSSBytes, idx.RSSFileBytes, idx.RSSAnonBytes)
}
b, err := json.Marshal(idx)
if err != nil {
t.Fatalf("marshal: %v", err)
}
const gpuMem = `"gpu_mem":[{"index":0,"used_bytes":11164188672,"vram_bytes":12884901888},{"index":1,"used_bytes":8589934592}]`
if !bytes.Contains(b, []byte(gpuMem)) {
t.Errorf("gpu_mem JSON = %s, want it to contain %s", b, gpuMem)
}
for _, key := range []string{`"rss_bytes":12456681472`, `"rss_file_bytes":11947323392`, `"rss_anon_bytes":263991296`} {
if !bytes.Contains(b, []byte(key)) {
t.Errorf("missing %s in %s", key, b)
}
}
// The unread capacity is absent, not a zero.
if bytes.Contains(b, []byte(`"vram_bytes":0`)) {
t.Errorf("vram_bytes 0 present in %s", b)
}
})

t.Run("nothing measured", func(t *testing.T) {
// A host inventory is not a reading. GPUsAtEnd empty and RSS 0
// leave every memory key off the wire, even when a file byte was
// filled in beside a zero RSS — the card prints Host RSS only when
// RSSBytes > 0.
s := tape.RunSummary{
Host: tape.HostInfo{GPUs: []tape.GPUInfo{
{Index: 0, Name: "NVIDIA GeForce RTX 3060", VRAMBytes: 12884901888},
}},
Memory: tape.MemorySummary{AtEnd: tape.MemSample{RSSFileBytes: 11947323392}},
}
b, err := json.Marshal(IndexOf(&tape.Tape{Summary: s}))
if err != nil {
t.Fatalf("marshal: %v", err)
}
for _, key := range []string{"gpu_mem", "rss_bytes", "rss_file_bytes", "rss_anon_bytes"} {
if bytes.Contains(b, []byte(`"`+key+`"`)) {
t.Errorf("unset field %q present in %s", key, b)
}
}
})

t.Run("unified host", func(t *testing.T) {
// The samples and the capacity are present, so a missing gpu_mem
// is the unified rule (tape.Unified), not an empty reading. RSS
// still travels: the card prints Host RSS on a unified host too.
s := tape.RunSummary{
Host: tape.HostInfo{
GPUs: []tape.GPUInfo{{Index: 0, Name: "NVIDIA GB10", VRAMBytes: 128849018880}},
},
GPUsAtEnd: []tape.GPUSample{{Index: 0, UsedBytes: 11164188672}},
Memory: tape.MemorySummary{AtEnd: tape.MemSample{
RSSBytes: 12456681472,
RSSFileBytes: 11947323392,
RSSAnonBytes: 263991296,
}},
}
if tape.Unified(s.Host) == "" {
t.Fatal("fixture is not unified; the case proves nothing")
}
b, err := json.Marshal(IndexOf(&tape.Tape{Summary: s}))
if err != nil {
t.Fatalf("marshal: %v", err)
}
if bytes.Contains(b, []byte(`"gpu_mem"`)) {
t.Errorf("gpu_mem present on a unified host: %s", b)
}
if !bytes.Contains(b, []byte(`"rss_bytes":12456681472`)) {
t.Errorf("rss dropped on a unified host: %s", b)
}
})
}
Loading
Loading