diff options
| author | Jonas Knobloch <jonas.knobloch@t-online.de> | 2026-09-11 18:39:02 +0200 |
|---|---|---|
| committer | Jonas Knobloch <jonas.knobloch@t-online.de> | 2026-09-11 18:39:02 +0200 |
| commit | 9e1b8c4bde9b0263a4c4d2278e3c283ca0eb07f1 (patch) | |
| tree | bf6b604a150a196490b92f89ca40a335aca83d31 /research/knobloch/cmd/tables/main.go | |
| parent | 75e581b3bc19a73d0c1f48b32f2e58121c9741c1 (diff) | |
Diffstat (limited to 'research/knobloch/cmd/tables/main.go')
| -rw-r--r-- | research/knobloch/cmd/tables/main.go | 220 |
1 files changed, 220 insertions, 0 deletions
diff --git a/research/knobloch/cmd/tables/main.go b/research/knobloch/cmd/tables/main.go new file mode 100644 index 0000000..27c3f0c --- /dev/null +++ b/research/knobloch/cmd/tables/main.go @@ -0,0 +1,220 @@ +// Command tables writes the tokenizer characterisation CSVs described in +// FREQUENCIES.md. +// +// Coverage is the full grid: every alignment level in both directions, at every +// vocabulary size we have tokenizers for. Statistics are over the MiniPile train +// dictionary. +// +// Encoding the dictionary dominates the runtime and the encodes are independent, +// so they run on a worker pool. One vocabulary size is processed at a time, +// since table C compares tokenizers within a size and the comparison inputs for +// a whole size have to be resident together. +package main + +import ( + "flag" + "fmt" + "log" + "runtime" + "sync" + + "github.com/jonasknobloch/mbpe" + "go.jknobloc.com/x/research/knobloch" + "go.jknobloc.com/x/shelf" +) + +var ( + vocabSizes = []int{8192, 16384, 32768, 50256, 100512} + alphas = []int{0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100} +) + +// name is the tokenizer directory suffix, e.g. m050 or mi050. +func name(alpha int, inverted bool) string { + if inverted { + return fmt.Sprintf("mi%03d", alpha) + } + + return fmt.Sprintf("m%03d", alpha) +} + +func spec(size, alpha int, inverted bool) knobloch.TableSpec { + n := name(alpha, inverted) + + return knobloch.TableSpec{ + Name: n, + Alignment: fmt.Sprintf("%.1f", float64(alpha)/100), + Inverted: inverted, + VocabSize: size, + Dir: shelf.Item(fmt.Sprintf("tokenizers/minipile/tokenizer_gpt2_%d_%s_minipile", size, n)), + } +} + +func main() { + dict := flag.String("dict", "results/knobloch/minipile/dict.txt", "shelf-relative dictionary") + prefix := flag.String("prefix", "table", "output file prefix") + workers := flag.Int("workers", runtime.NumCPU(), "parallel encodes") + only := flag.Int("only", 0, "restrict to a single vocabulary size, for smoke runs") + + flag.Parse() + + sizes := vocabSizes + + if *only != 0 { + sizes = []int{*only} + } + + d := mbpe.NewDict() + + if err := d.Load(shelf.Abs(shelf.Item(*dict))); err != nil { + log.Fatal(err) + } + + items := d.Items() + + log.Printf("dictionary: %d pre-token types", len(items)) + + var ( + rowsA []knobloch.TableARow + rowsB []knobloch.TableBRow + + aligned []knobloch.TableCRow + inverted []knobloch.TableCRow + invVsAlign []knobloch.TableCRow + ) + + for _, size := range sizes { + // every tokenizer at this size, aligned then inverted + var specs []knobloch.TableSpec + + for _, inv := range []bool{false, true} { + for _, a := range alphas { + specs = append(specs, spec(size, a, inv)) + } + } + + dirs := make([]shelf.Item, 0, len(specs)) + + for _, s := range specs { + dirs = append(dirs, s.Dir) + } + + // table B measures sharing against the intersection of every vocabulary + // at this size, so the baseline is just another tokenizer + shared, err := knobloch.VocabIntersection(dirs) + + if err != nil { + log.Fatal(err) + } + + log.Printf("vocab %d: %d tokenizers, intersection %d tokens", size, len(specs), len(shared)) + + encoded := make([]*knobloch.Encoded, len(specs)) + + var wg sync.WaitGroup + + queue := make(chan int) + + for i := 0; i < *workers; i++ { + wg.Add(1) + + go func() { + defer wg.Done() + + for idx := range queue { + e, err := knobloch.EncodeDict(specs[idx], items) + + if err != nil { + log.Fatal(err) + } + + encoded[idx] = e + } + }() + } + + for i := range specs { + queue <- i + } + + close(queue) + wg.Wait() + + log.Printf("vocab %d: encoded", size) + + // index by (alpha, direction) for the pairings below + at := func(alpha int, inv bool) *knobloch.Encoded { + for i, s := range specs { + if s.Inverted == inv && s.Name == name(alpha, inv) { + return encoded[i] + } + } + + log.Fatalf("missing encode for %d/%s", size, name(alpha, inv)) + + return nil + } + + base := at(0, false) + + for _, e := range encoded { + rowsA = append(rowsA, knobloch.BuildTableA(e)) + + b, err := knobloch.BuildTableB(e, shared) + + if err != nil { + log.Fatal(err) + } + + rowsB = append(rowsB, b) + } + + for _, a := range alphas { + al := at(a, false) + inv := at(a, true) + + // aligned vs the alpha=0 baseline + r, err := knobloch.BuildTableC(al, base, items) + + if err != nil { + log.Fatal(err) + } + + aligned = append(aligned, r) + + // inverted vs the alpha=0 baseline + r, err = knobloch.BuildTableC(inv, base, items) + + if err != nil { + log.Fatal(err) + } + + inverted = append(inverted, r) + + // inverted vs aligned at matching alpha + r, err = knobloch.BuildTableC(inv, al, items) + + if err != nil { + log.Fatal(err) + } + + invVsAlign = append(invVsAlign, r) + } + } + + for _, out := range []struct { + name string + err error + }{ + {*prefix + "_a.csv", knobloch.WriteTableA(*prefix+"_a.csv", rowsA)}, + {*prefix + "_b.csv", knobloch.WriteTableB(*prefix+"_b.csv", rowsB)}, + {*prefix + "_c_aligned.csv", knobloch.WriteTableC(*prefix+"_c_aligned.csv", aligned)}, + {*prefix + "_c_inverted.csv", knobloch.WriteTableC(*prefix+"_c_inverted.csv", inverted)}, + {*prefix + "_c_inverted_vs_aligned.csv", knobloch.WriteTableC(*prefix+"_c_inverted_vs_aligned.csv", invVsAlign)}, + } { + if out.err != nil { + log.Fatal(out.err) + } + + log.Printf("wrote %s", out.name) + } +} |
