Skip to content
Closed
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
52 changes: 30 additions & 22 deletions .github/workflows/no-gpu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ on:
branches: [main]
pull_request:
types: [opened, synchronize, reopened]
# Each night builds and runs everything from nothing: what a reused pass could hide, such as a test reading a file
# the build doesn't know of, fails here within a day.
schedule:
- cron: "23 3 * * *"

permissions:
contents: read
Expand Down Expand Up @@ -46,6 +50,7 @@ jobs:
python3 -m unittest discover -s tools/zig -p cuda_common_check_test.py -v
# The lean rule (600-line files, one-line comments within 120 columns): no problem the base branch lacks.
- name: lean check
if: ${{ github.event_name != 'schedule' }}
env:
BASE_REF: ${{ github.base_ref }}
PUSH_BASE: ${{ github.event.before }}
Expand All @@ -64,20 +69,21 @@ jobs:
with:
version: 0.17.0
use-cache: false
# Main builds from nothing, so every commit on main is checked from scratch and the cache holds one build. A pull
# request restores main's newest and saves nothing, so it compiles and runs only what its diff changed.
# Main and pull requests restore main's newest build, so each compiles and runs only what changed since; only main
# saves, and the nightly run starts from nothing.
# -Dfixed-cpu: a compile's cache key carries its CPU and these runners differ, so all of them compile for one CPU.
- if: ${{ github.event_name == 'pull_request' }}
- if: ${{ github.event_name != 'schedule' }}
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .zig-cache
key: zig-build-v3-Linux-0.17.0-${{ github.sha }}
restore-keys: zig-build-v3-Linux-0.17.0-
# A checkout or a restored cache gives every file a new id, and Zig 0.17's cache misses on one even when the
# contents match (fixed on Zig's master, ziglang/zig#37238). A failure here costs time, never a check.
# --prune deletes the entries this checkout can't hit, so a cache that main restores and saves stays one build.
- name: Let a restored cache fit this checkout
shell: bash
run: zig run -OReleaseFast -mcpu=baseline tools/zig/zig_cache_restat.zig -- .zig-cache "$(dirname "$(command -v zig)")/lib" || echo "::warning::zig_cache_restat failed, so this job rebuilds and reruns more than it needs to"
run: zig run -OReleaseFast -mcpu=baseline tools/zig/zig_cache_restat.zig -- .zig-cache "$(dirname "$(command -v zig)")/lib" --prune || echo "::warning::zig_cache_restat failed, so this job rebuilds and reruns more than it needs to"
# The restat tool's own test, when a change touches the tool, this workflow or Zig's version.
- name: Restored cache regressions
shell: bash
Expand Down Expand Up @@ -134,11 +140,11 @@ jobs:
- name: build options
run: python3 -m unittest discover -s tools/zig -p build_options_test.py
- name: Save the build cache
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
if: ${{ !cancelled() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .zig-cache
key: zig-build-v3-Linux-0.17.0-${{ github.sha }}
key: zig-build-v3-Linux-0.17.0-${{ github.sha }}${{ github.event_name == 'schedule' && '-nightly' || '' }}
zig-windows:
runs-on: windows-latest
timeout-minutes: 60
Expand All @@ -150,18 +156,18 @@ jobs:
with:
version: 0.17.0
use-cache: false
# Main builds from nothing, so every commit on main is checked from scratch and the cache holds one build. A pull
# request restores main's newest and saves nothing, so it compiles and runs only what its diff changed.
# Main and pull requests restore main's newest build, so each compiles and runs only what changed since; only main
# saves, and the nightly run starts from nothing.
# -Dfixed-cpu: a compile's cache key carries its CPU and these runners differ, so all of them compile for one CPU.
- if: ${{ github.event_name == 'pull_request' }}
- if: ${{ github.event_name != 'schedule' }}
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .zig-cache
key: zig-build-v3-Windows-0.17.0-${{ github.sha }}
restore-keys: zig-build-v3-Windows-0.17.0-
- name: Let a restored cache fit this checkout
shell: bash
run: zig run -OReleaseFast -mcpu=baseline tools/zig/zig_cache_restat.zig -- .zig-cache "$(dirname "$(command -v zig)")/lib" || echo "::warning::zig_cache_restat failed, so this job rebuilds and reruns more than it needs to"
run: zig run -OReleaseFast -mcpu=baseline tools/zig/zig_cache_restat.zig -- .zig-cache "$(dirname "$(command -v zig)")/lib" --prune || echo "::warning::zig_cache_restat failed, so this job rebuilds and reruns more than it needs to"
# The same host unit tests on Windows: the platform layer (zig/src/platform) over the UCRT, Win32 and Winsock.
- name: zig build test
run: zig build -Dfixed-cpu test --test-timeout 10m
Expand All @@ -171,18 +177,18 @@ jobs:
- name: zig build fuzz-cortheon
run: zig build -Dfixed-cpu fuzz-cortheon
- name: Save the build cache
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
if: ${{ !cancelled() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .zig-cache
key: zig-build-v3-Windows-0.17.0-${{ github.sha }}
key: zig-build-v3-Windows-0.17.0-${{ github.sha }}${{ github.event_name == 'schedule' && '-nightly' || '' }}
# The release's own path check on each platform's dist server (no fatbins), so a leak fails here, not at release.
# Pull requests check Linux x86_64 only; every push to main checks all three platforms.
# Pull requests check Linux x86_64 only; main's pushes and the nightly run check all three platforms.
release-paths:
strategy:
fail-fast: false
matrix:
os: ${{ github.event_name == 'push' && fromJSON('["macos-26", "ubuntu-24.04", "ubuntu-24.04-arm"]') || fromJSON('["ubuntu-24.04"]') }}
os: ${{ github.event_name != 'pull_request' && fromJSON('["macos-26", "ubuntu-24.04", "ubuntu-24.04-arm"]') || fromJSON('["ubuntu-24.04"]') }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
steps:
Expand All @@ -200,7 +206,8 @@ jobs:
env:
SHARED: ${{ hashFiles('zig/src/core/**', 'zig/src/platform/**', 'zig/src/hip/**', 'zig/src/*.zig') }}
run: echo "shared=$SHARED" >> "$GITHUB_OUTPUT"
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
- if: ${{ github.event_name != 'schedule' }}
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .zig-cache
key: zig-build-v3-paths-${{ matrix.os }}-0.17.0-${{ steps.cache-key.outputs.shared }}-${{ github.sha }}
Expand All @@ -209,18 +216,18 @@ jobs:
${{ github.event_name == 'pull_request' && format('zig-build-v3-paths-{0}-0.17.0-', matrix.os) || '' }}
- name: Let a restored cache fit this checkout
shell: bash
run: zig run -OReleaseFast -mcpu=baseline tools/zig/zig_cache_restat.zig -- .zig-cache "$(dirname "$(command -v zig)")/lib" || echo "::warning::zig_cache_restat failed, so this job rebuilds and reruns more than it needs to"
run: zig run -OReleaseFast -mcpu=baseline tools/zig/zig_cache_restat.zig -- .zig-cache "$(dirname "$(command -v zig)")/lib" --prune || echo "::warning::zig_cache_restat failed, so this job rebuilds and reruns more than it needs to"
- name: Metal toolchain
if: runner.os == 'macOS'
run: xcrun -sdk macosx metal -v || { xcodebuild -downloadComponent MetalToolchain && xcrun -sdk macosx metal -v; }
- name: zig build check-release-paths
run: zig build -Dfixed-cpu check-release-paths
- name: Save the build cache
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
if: ${{ !cancelled() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .zig-cache
key: zig-build-v3-paths-${{ matrix.os }}-0.17.0-${{ steps.cache-key.outputs.shared }}-${{ github.sha }}
key: zig-build-v3-paths-${{ matrix.os }}-0.17.0-${{ steps.cache-key.outputs.shared }}-${{ github.sha }}${{ github.event_name == 'schedule' && '-nightly' || '' }}
zig-macos-dev-tools:
runs-on: macos-26
timeout-minutes: 45
Expand All @@ -239,7 +246,8 @@ jobs:
env:
SHARED: ${{ hashFiles('zig/src/core/**', 'zig/src/platform/**', 'zig/src/hip/**', 'zig/src/*.zig') }}
run: echo "shared=$SHARED" >> "$GITHUB_OUTPUT"
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
- if: ${{ github.event_name != 'schedule' }}
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .zig-cache
key: zig-build-v3-macOS-dev-tools-0.17.0-${{ steps.cache-key.outputs.shared }}-${{ github.sha }}
Expand All @@ -248,16 +256,16 @@ jobs:
${{ github.event_name == 'pull_request' && 'zig-build-v3-macOS-dev-tools-0.17.0-' || '' }}
- name: Let a restored cache fit this checkout
shell: bash
run: zig run -OReleaseFast -mcpu=baseline tools/zig/zig_cache_restat.zig -- .zig-cache "$(dirname "$(command -v zig)")/lib" || echo "::warning::zig_cache_restat failed, so this job rebuilds and reruns more than it needs to"
run: zig run -OReleaseFast -mcpu=baseline tools/zig/zig_cache_restat.zig -- .zig-cache "$(dirname "$(command -v zig)")/lib" --prune || echo "::warning::zig_cache_restat failed, so this job rebuilds and reruns more than it needs to"
# Xcode 26 ships the Metal compiler as a separate component, which the image does not always carry.
- name: Metal toolchain
run: xcrun -sdk macosx metal -v || { xcodebuild -downloadComponent MetalToolchain && xcrun -sdk macosx metal -v; }
# Every tf-* developer tool compiled and linked, none installed or run: an engine API change that breaks one fails.
- name: zig build dev-tools
run: zig build dev-tools -Doptimize=Debug # compile and link only: Debug builds about 30% faster
- name: Save the build cache
if: ${{ !cancelled() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
if: ${{ !cancelled() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .zig-cache
key: zig-build-v3-macOS-dev-tools-0.17.0-${{ steps.cache-key.outputs.shared }}-${{ github.sha }}
key: zig-build-v3-macOS-dev-tools-0.17.0-${{ steps.cache-key.outputs.shared }}-${{ github.sha }}${{ github.event_name == 'schedule' && '-nightly' || '' }}
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Each release's page on GitHub has its notes and measurements. See [the 1.0.0 rel

## Unreleased

- Docs: ten native `serve` flags that were undocumented now are: `--top-p`, `--top-k` and `--min-p` as sampling defaults, `--compact-at`, `--compact-keep` and `--compact-memory`, Dory's `--loops`, the two silence-onset flags and Drama 3's `--max-frames` (#2345).

- CI: main's `zig` and `zig-windows` jobs reuse main's newest build cache, as a pull request does, so each push compiles and runs only what changed since. `zig_cache_restat --prune` keeps each saved cache to about one build, and a nightly run builds and checks everything from nothing (#2402).

- Docs: `docs/api.md` lists `DELETE /v1/slide/facts`, and `docs/control.md` no longer says the native server refuses `--spill-gib` and `--vision` (#2341).

- Prompt cache on CUDA: when a kept state is larger than the whole cache budget, the log line now says that `TF_KV_SHARE=n` would keep its prompt rows as the pool's pages, for families on KV pages; nothing else changes (#2333). Thanks to @Bizuayeu.
Expand Down
38 changes: 38 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,17 @@ request yields when a foreground request needs its lane or memory, then restarts
suppressed.
Session-title requests are also treated as background work.

The serve flags `--top-p`, `--top-k` and `--min-p` set the server's defaults for the request fields of the same
names, as `--temperature` does. Under them sit two layers: a family's built-in sampling (DeepSeek-V4 and V4.1
serve their model cards' temperature 1 and top_p 1), then the checkpoint's `generation_config.json`
(`temperature`, `top_k`, `top_p`, `min_p`; `do_sample: false` means temperature 0, and `do_sample: true` without
a temperature means 1). A flag that is set replaces what those layers gave. When nothing sets a value the server
has no sampling default and the serving line prints `greedy`; otherwise it prints the resolved defaults as
name-value pairs. A request's own fields win over the defaults. The merged values are checked per request like
request fields: a `--min-p` outside 0 through 1 refuses every chat request, a negative `--top-k` clamps to 0, and
`--temperature` and `--top-p` must be finite numbers. An engine that decodes greedily only refuses sampled
requests while the server samples by default, and its refusal suggests serving with `--temperature 0`.

`--name-priority ID=background` gives one served id (`--name` or an `--alias`) a default priority: a request that
asks for it and sends no `priority` of its own is treated as `priority: "background"`. The request's own `priority`
field always wins over the default. This lets one server answer to several ids at their usual priority while a
Expand Down Expand Up @@ -602,6 +613,33 @@ On a discrete card (an RTX 3090, an A100) the server sizes its window from the c
reports it, less a reserve of a 32nd of the card and at least 1 GiB. `TENSORFOLD_MEMORY_RESERVE_GIB` sets that one from
0.5 GiB up.

## Compaction

Without `--compact-at` nothing compacts: a conversation that outgrows the context is refused with the usual
`context_length` error. With it, the server summarizes instead of refusing. `--compact-at auto` reserves room for
the reply — the larger of 15% of the context window and 16,384 tokens, or 15% when the window can't hold 16,384 — and compacts a conversation whose prompt
plus wanted reply tokens would cross what is left, or one the plain prepare refuses for context length.
`--compact-at F`, a fraction of the window in (0, 1], compacts when they would cross F × the window, rounded down.
Any other value is refused at startup ("expected auto or a fraction in (0, 1]").

Compaction keeps the conversation's leading messages and its recent tail whole and summarizes the middle into a
memory note. The note is written by the model itself — greedy, thinking off, at most 1,024 tokens a chunk — under
a task that asks for Markdown sections (Goal, Constraints and preferences, Progress, Decisions, Next steps,
Critical context) and under 800 words. It replaces the summarized middle as one message that begins "Memory of
the earlier conversation:". The tail is `--compact-keep` tokens of recent messages, an integer from 0; the
default keeps the smaller of 20,000 tokens and a quarter of the window. If the rebuilt conversation still does
not fit, the server summarizes again over a smaller tail until it does. A compacted request runs these extra
generations before its reply.

`--compact-memory DIR` stores each conversation's note under DIR as `<key>.md`, with a `<key>.json` recording how
many leading messages the note covers and their hash. A later request whose leading messages hash the same reuses
the stored note instead of regenerating it. Without the flag a note lives for one request.

The reply's `tensorfold` object carries `context_window` and `context_used`, and when compaction ran also a
`compaction` object: `messages_compacted`, `kept_messages`, `note_tokens` and the `note` text. Compaction applies
to chat completions with a `messages` array, and to the Responses API, which runs as one; a completions request
with a plain string prompt is never compacted.

## Embeddings

`POST /v1/embeddings` takes OpenAI's body: `input` is one text, a list of texts, one list of token IDs or a list of
Expand Down
9 changes: 9 additions & 0 deletions docs/recipes/cuda.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,15 @@ staggered prompt work for 1 through 4 whole 2,048-row chunks; without the flag i
uses `TF_CUDA_SEGMENTS`, then 1. The `tensorfold segments` command compares the
available segment counts and can profile the serial chunk parts.

Dory's recurrent block can run more than once a pass: `--loops N` serves the checkpoint with N passes over it,
1 through 4. Without the flag a checkpoint serves at its own `n_recurrent_loops`; the flag overrides it. The
schedule runs the input sublayers once, the recurrent block N times, then the output sublayers — 16 + 40×N + 16
steps with 8 + 20×N + 8 attention caches — and a token's KV and stream state grows from 36 KiB at one loop to 56
at two and 96 at four. A saved state records its loop count and never crosses to another. Zero and anything above
4 are refused at load ("--loops: passes over Dory's recurrent block, 1 to 4"); a negative count is refused at
parse. The other CUDA families, the HIP engines and Kimi on Metal refuse the flag by name, and `tensorfold run`
refuses it for any model that is not Dory.

Experimental: on a DGX Spark (GB10), `tensorfold run --carveout`, or `TF_CUDA_CARVEOUT=1` for `tensorfold run` and
`tensorfold-native`, puts Nemotron's KV caches in the memory the display controller reserves, which
MemAvailable never counts. The native engine maps a
Expand Down
7 changes: 7 additions & 0 deletions docs/speech-redraws.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ A floor of -30 dBFS also catches the hum. Streams are not redrawn (see
[Streams](#streams)): a stalled stream ends early, with the rest of the text
unspoken.

Two more flags cut a reply that never really starts. With `--silence-onset-s S` and `--silence-onset-voiced-s V`
(both required together, and both need the floor and `--silence-seconds`; S must be above 0 and below the
silence seconds, V above 0), a reply also ends once it has stayed S seconds below the floor while having voiced
less than V seconds in total — the model cleared its throat but never began speaking. The startup line names the
rule: "or after S s below it with under V s voiced". An onset trip is a `silence` finish, redrawn like any
other.

## Seeds

Without an explicit request `seed`, redraw k (1 to N) seeds its prompt with `TENSORFOLD_SEED_SALT` plus k; the first
Expand Down
9 changes: 9 additions & 0 deletions docs/speech-sampling.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ number are unchanged. Streams keep their existing no-redraw behavior.
Seed selection is deterministic. The final take can still change if timing or
request deadlines admit a different number of redraws.

The server's `--max-frames` flag caps a reply's length in frames. A frame is 2,048 samples at 44.1 kHz (about
46 ms), so the default cap of 1,024 frames is about 48 s of audio. The flag takes an integer from 1 through 8190
("argument --max-frames: expected 1 through 8190") and Drama 3 reads it at open: the reply's cap is the flag's
value (1,024 when it's unset), clamped to what the context leaves (`--context` - 3 frames). An explicit
value must also leave prompt room — one at or above `--context` - 1 is refused ("use --max-frames <
--context - 1") — and Drama 3's `--context` itself must stay within 1025 through 8192. Other engines ignore the
flag, and video input is untouched: the vision families' frame caps are their own (see
[vision](vision.md)).

The API design references the public
[Sogni-AI/drama3-mlx generation interface](https://github.com/Sogni-AI/drama3-mlx/blob/ecc235039ccd5f982f66a6982184f57432354ab4/drama3_mlx/model.py#L79).
TensorFold's parsing and request propagation are an independent implementation;
Expand Down
Loading
Loading