diff --git a/CMakeLists.txt b/CMakeLists.txt index d366e8ea..2191715f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -111,6 +111,16 @@ endif() option(TRANSCRIBE_BUILD_TESTS "Build unit / smoke tests" ON) option(TRANSCRIBE_BUILD_EXAMPLES "Build example CLI" ON) option(TRANSCRIBE_BUILD_TOOLS "Build inspect / quantize" OFF) # post pass 2 +# Numerical-parity validation hooks: reference-mel injection (TRANSCRIBE_MEL_FROM_REF) +# and per-layer tensor dumps (TRANSCRIBE_DUMP_ALL_BLOCKS / _SUB_BLOCKS). OFF for +# release so the hook code is not compiled into shipped binaries — the env-var +# reads, ref-mel loaders, and dump scaffolding all live behind this guard, so +# their names do not even appear in a release binary's strings. The validation +# pipeline (scripts/validate.py, the Modal sweep image) builds with this ON; +# validate.py hard-fails --mel-from-ref against an OFF build rather than +# silently ignoring it. +option(TRANSCRIBE_ENABLE_VALIDATION_HOOKS + "Compile in numerical-parity validation hooks" OFF) option(TRANSCRIBE_METAL "Enable Metal backend" ${TRANSCRIBE_IS_APPLE_SILICON}) option(TRANSCRIBE_VULKAN "Enable Vulkan backend" OFF) # post-v1 option(TRANSCRIBE_CUDA "Enable CUDA backend" OFF) # opt-in; Linux + nvcc @@ -182,7 +192,7 @@ option(TRANSCRIBE_INSTALL # Real-model gated tests. OFF by default because the tests need a # converted Parakeet GGUF on disk (~2.4 GB) and CI can't ship one. -# When ON, the test reads TRANSCRIBE_REAL_PARAKEET_GGUF from the +# When ON, the test reads TRANSCRIBE_PARAKEET_GGUF from the # environment at run time. See tests/parakeet_real_smoke.cpp. option(TRANSCRIBE_BUILD_REAL_MODEL_TESTS "Build tests that load real (multi-GB) model files via env var" OFF) diff --git a/CMakePresets.json b/CMakePresets.json index 2229bfb5..334f11ce 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -56,6 +56,15 @@ "inherits": "wheel-linux-cpu-vulkan", "displayName": "Windows x86_64 default wheel: CPU + Vulkan backend modules", "description": "Same provider shape as wheel-linux-cpu-vulkan on Windows: conservative CPU module + Vulkan module, loaded package-locally (the Python loader calls os.add_dll_directory before CDLL). Build image needs the Vulkan SDK (glslc)." + }, + { + "name": "validation", + "displayName": "Local numerical-parity validation build (build/)", + "description": "Dev build for scripts/validate.py: enables TRANSCRIBE_ENABLE_VALIDATION_HOOKS so the reference-mel injection (TRANSCRIBE_MEL_FROM_REF) and per-layer tensor dumps (TRANSCRIBE_DUMP_ALL_BLOCKS / TRANSCRIBE_DUMP_SUB_BLOCKS) are compiled in. Targets build/ (the dir validate.py runs build/bin/transcribe-cli from) and reuses the existing generator. NOT for release — shipped and wheel builds leave the hooks out (TRANSCRIBE_ENABLE_VALIDATION_HOOKS defaults OFF).", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "TRANSCRIBE_ENABLE_VALIDATION_HOOKS": "ON" + } } ] } diff --git a/README.md b/README.md index 50de6fb1..f922c55e 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Some tests require a real model file. Enable them with: ```bash cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_PARAKEET_GGUF=path/to/model.gguf ctest --test-dir build +TRANSCRIBE_PARAKEET_GGUF=path/to/model.gguf ctest --test-dir build ``` For the model-family smoke-test, numerical-validation, and benchmark diff --git a/docs/environment-variables.md b/docs/environment-variables.md new file mode 100644 index 00000000..c1f21d78 --- /dev/null +++ b/docs/environment-variables.md @@ -0,0 +1,91 @@ +# Environment Variables + +A single reference for the environment variables transcribe.cpp recognizes, +split by audience. The library follows one parse convention for boolean +toggles: a flag is **on** when it is set, non-empty, and its first character is +not `0` (so `FOO=1`, `FOO=on`, `FOO=yes` are on; `FOO=0`, `FOO=` and unset are +off). Path-valued vars are off when unset or empty. + +## Tier 1 — runtime configuration + +Compiled into every build, including release. These are operational knobs, not +build-gated. Note that `TRANSCRIBE_DUMP_DIR` — while it ships — is still a +tensor-dumping debug/validation hook: enabling it does per-tensor device→host +copies and writes raw activations to disk, so it carries I/O cost and a +data-leak surface. Leave it unset in production unless you are deliberately +collecting dumps. + +| Variable | Effect | +| --- | --- | +| `TRANSCRIBE_NO_FLASH` | Disable flash attention on encoder and decoder (forces the manual F32 path). | +| `TRANSCRIBE_FORCE_FLASH` | Force flash attention on. Wins over `TRANSCRIBE_NO_FLASH` if both are set. | +| `TRANSCRIBE_CONV_DIRECT_DW` / `TRANSCRIBE_CONV_NO_DIRECT_DW` | Force the depthwise-conv dispatch to the direct `conv_2d_dw` path / the im2col path, overriding the per-family backend default. | +| `TRANSCRIBE_CONV_DIRECT_PW` / `TRANSCRIBE_CONV_NO_DIRECT_PW` | Force the pointwise-conv dispatch to direct `mul_mat` / im2col, overriding the backend default. | +| `TRANSCRIBE_DUMP_DIR=` | Enable the per-stage tensor dumper; writes `.f32` + `.json` per dumped tensor into ``. The basis for the numerical-comparison harness (`scripts/compare_tensors.py`). | +| `TRANSCRIBE_PERF_DEBUG` | Print a per-stage timing breakdown to stderr (DEBUG log) on the families that profile (`cohere`, `granite`, `canary`, `canary_qwen`, `moonshine`, `moonshine_streaming`, `qwen3_asr`, `whisper`). For whisper, a value containing `cpu` or `all` additionally prints the CPU sub-section breakdown. | +| `TRANSCRIBE_VOXTRAL_REALTIME_STREAM_TIMING` | Print a per-component streaming wall-time breakdown at stream finalize (voxtral_realtime). | + +## Tier 2 — validation hooks + +Numerical-parity hooks for porting and validation. **Compiled in only when the +library is built with `-DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON`** (see the CMake +option of the same name, or the `validation` preset). Release/wheel builds leave +them out entirely — the env-var reads, ref-mel loaders, and dump scaffolding are +all behind the compile guard, so these names do not even appear in a release +binary. In a build *without* the flag, setting any of these has no effect; +`scripts/validate.py` hard-fails `--mel-from-ref` against such a build rather +than silently falling back. + +| Variable | Effect | +| --- | --- | +| `TRANSCRIBE_MEL_FROM_REF=` | Inject a reference log-mel from `` instead of computing it, so encoder drift can be isolated from frontend drift. Each family reads its own dump filename/layout from the directory (`enc.mel.in.f32` for whisper / voxtral_realtime, `frontend.mel.out.f32` for gigaam, `mel.in.f32` for medasr). Driven by `scripts/validate.py --mel-from-ref`. | +| `TRANSCRIBE_DUMP_ALL_BLOCKS` | Dump every encoder block output (not just mid/last) for a layer-by-layer divergence bisect (parakeet, gigaam). Requires `TRANSCRIBE_DUMP_DIR`. | +| `TRANSCRIBE_DUMP_SUB_BLOCKS=` | Dump intermediate sub-layer activations (ff1/attn/conv/ff2) for the listed block indices, e.g. `0,12,23` (parakeet). Requires `TRANSCRIBE_DUMP_DIR`. | + +Build a validation-capable `build/` with: + +```bash +cmake --preset validation # configures build/ with the hooks ON +cmake --build build --target transcribe-cli +``` + +When **toggling** `TRANSCRIBE_ENABLE_VALIDATION_HOOKS` on an existing `build/`, +do a clean build of the target — an incremental build can leave a stale object +that did not pick up the changed compile definition: + +```bash +cmake --build build --clean-first --target transcribe-cli +``` + +(`scripts/validate.py` guards against the OFF case: it hard-fails +`--mel-from-ref` when `build/CMakeCache.txt` has the flag off. Longer term, a +dedicated validation build directory would avoid the toggle entirely.) + +## Test & tooling + +Not part of the library runtime. + +**Real-model test model paths.** Built only with +`-DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON`; each test skips (exit code 77) when +its var is unset. Convention: `TRANSCRIBE__GGUF`. + +| Variable | Test(s) | +| --- | --- | +| `TRANSCRIBE_PARAKEET_GGUF` | `parakeet_real_smoke`, `decoder_smoke` | +| `TRANSCRIBE_COHERE_GGUF` | `cohere_real_smoke`, `cohere_e2e_smoke` | +| `TRANSCRIBE_WHISPER_GGUF` | `whisper_e2e_smoke`, `whisper_tokenize_parity` | +| `TRANSCRIBE_QWEN3_ASR_GGUF` (+ `_0_6B_GGUF` / `_1_7B_GGUF`) | qwen3_asr smokes / parity | +| `TRANSCRIBE_MOONSHINE_STREAMING_TINY_GGUF` | moonshine_streaming smokes | +| `TRANSCRIBE_VOXTRAL_REALTIME_GGUF` | `voxtral_realtime_real_smoke` | +| `TRANSCRIBE_WHISPER_BIN_*` | whisper.cpp `.bin` parser/e2e fixtures | + +Other test/tooling vars: + +- `TRANSCRIBE_TEST_AUDIO` — override the `jfk.wav` path for ad-hoc e2e runs. +- `TRANSCRIBE_TEST_SAMPLES_DIR` / `TRANSCRIBE_TEST_FIXTURES_DIR` — sample and + fixture directories, passed at configure time as compile definitions (not + runtime env vars). +- Python tooling: `TRANSCRIBE_MODELS_DIR`, `TRANSCRIBE_DUMP_DIR` (shared with + the library), `HF_TOKEN`, and the binding-level `TRANSCRIBE_BACKEND` / + `TRANSCRIBE_LIBRARY`. See [`tools/conversion.md`](tools/conversion.md) and + [`tools/validate.md`](tools/validate.md). diff --git a/docs/model-family-testing.md b/docs/model-family-testing.md index 2f355188..4c5ebf03 100644 --- a/docs/model-family-testing.md +++ b/docs/model-family-testing.md @@ -53,7 +53,9 @@ Expected shape: - Built only when `TRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON`. - Skips with return code `77` when the model path is not available. -- Uses one family-specific environment variable for the model path. +- Uses one family-specific environment variable for the model path, + following the `TRANSCRIBE__GGUF` convention (see + [`environment-variables.md`](environment-variables.md)). - Verifies architecture string, variant string if applicable, capabilities, hparams, language list, expected tensor count or tensor-table coverage, and canonical weight shapes. diff --git a/docs/models/cohere-transcribe-03-2026.md b/docs/models/cohere-transcribe-03-2026.md index dee43432..f605605b 100644 --- a/docs/models/cohere-transcribe-03-2026.md +++ b/docs/models/cohere-transcribe-03-2026.md @@ -179,6 +179,6 @@ uv run scripts/validate.py all --family cohere cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_COHERE_MODEL=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf \ +TRANSCRIBE_COHERE_GGUF=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf \ ctest --test-dir build --output-on-failure -R 'cohere' ``` diff --git a/docs/models/parakeet-tdt-0.6b-v2.md b/docs/models/parakeet-tdt-0.6b-v2.md index 0cecb2b0..87ada6d3 100644 --- a/docs/models/parakeet-tdt-0.6b-v2.md +++ b/docs/models/parakeet-tdt-0.6b-v2.md @@ -164,6 +164,6 @@ uv run scripts/validate.py all --family parakeet cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v2/parakeet-tdt-0.6b-v2-F32.gguf \ +TRANSCRIBE_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v2/parakeet-tdt-0.6b-v2-F32.gguf \ ctest --test-dir build --output-on-failure -R 'parakeet|encoder|decoder' ``` diff --git a/docs/models/parakeet-tdt-0.6b-v3.md b/docs/models/parakeet-tdt-0.6b-v3.md index baa3cfce..567fd42a 100644 --- a/docs/models/parakeet-tdt-0.6b-v3.md +++ b/docs/models/parakeet-tdt-0.6b-v3.md @@ -166,6 +166,6 @@ uv run scripts/validate.py all --family parakeet --variant parakeet-tdt-0.6b-v3 cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v3/parakeet-tdt-0.6b-v3-F32.gguf \ +TRANSCRIBE_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v3/parakeet-tdt-0.6b-v3-F32.gguf \ ctest --test-dir build --output-on-failure -R 'parakeet|encoder|decoder' ``` diff --git a/docs/models/whisper-base.en.md b/docs/models/whisper-base.en.md index 925cf4f6..af23eece 100644 --- a/docs/models/whisper-base.en.md +++ b/docs/models/whisper-base.en.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-base.en cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-base.en/whisper-base.en-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-base.en/whisper-base.en-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-base.md b/docs/models/whisper-base.md index c1549545..a01e31ec 100644 --- a/docs/models/whisper-base.md +++ b/docs/models/whisper-base.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-base cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-base/whisper-base-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-base/whisper-base-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-large-v2.md b/docs/models/whisper-large-v2.md index 2dd25a3e..974d4027 100644 --- a/docs/models/whisper-large-v2.md +++ b/docs/models/whisper-large-v2.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-large-v2 cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-large-v2/whisper-large-v2-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-large-v2/whisper-large-v2-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-large-v3-turbo.md b/docs/models/whisper-large-v3-turbo.md index b6716562..797a9d02 100644 --- a/docs/models/whisper-large-v3-turbo.md +++ b/docs/models/whisper-large-v3-turbo.md @@ -174,6 +174,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-large-v3-turbo cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-large-v3-turbo/whisper-large-v3-turbo-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-large-v3-turbo/whisper-large-v3-turbo-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-large-v3.md b/docs/models/whisper-large-v3.md index d12d7f75..5fc3fd02 100644 --- a/docs/models/whisper-large-v3.md +++ b/docs/models/whisper-large-v3.md @@ -174,6 +174,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-large-v3 cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-large-v3/whisper-large-v3-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-large-v3/whisper-large-v3-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-large.md b/docs/models/whisper-large.md index f4aa17c7..8306a0a3 100644 --- a/docs/models/whisper-large.md +++ b/docs/models/whisper-large.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-large cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-large/whisper-large-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-large/whisper-large-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-medium.en.md b/docs/models/whisper-medium.en.md index 54293730..3842e1d1 100644 --- a/docs/models/whisper-medium.en.md +++ b/docs/models/whisper-medium.en.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-medium.en cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-medium.en/whisper-medium.en-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-medium.en/whisper-medium.en-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-medium.md b/docs/models/whisper-medium.md index 38dad1c7..62b2c725 100644 --- a/docs/models/whisper-medium.md +++ b/docs/models/whisper-medium.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-medium cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-medium/whisper-medium-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-medium/whisper-medium-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-small.en.md b/docs/models/whisper-small.en.md index b2918787..8297555c 100644 --- a/docs/models/whisper-small.en.md +++ b/docs/models/whisper-small.en.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-small.en cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-small.en/whisper-small.en-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-small.en/whisper-small.en-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-small.md b/docs/models/whisper-small.md index f39acea8..ac8c44e0 100644 --- a/docs/models/whisper-small.md +++ b/docs/models/whisper-small.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-small cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-small/whisper-small-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-small/whisper-small-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-tiny.en.md b/docs/models/whisper-tiny.en.md index f2981f67..f1bb0b10 100644 --- a/docs/models/whisper-tiny.en.md +++ b/docs/models/whisper-tiny.en.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-tiny.en cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-tiny.en/whisper-tiny.en-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-tiny.en/whisper-tiny.en-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/models/whisper-tiny.md b/docs/models/whisper-tiny.md index 9649ab75..9510ddfb 100644 --- a/docs/models/whisper-tiny.md +++ b/docs/models/whisper-tiny.md @@ -175,6 +175,6 @@ uv run scripts/validate.py all --family whisper --variant whisper-tiny cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_WHISPER_GGUF=$PWD/models/whisper-tiny/whisper-tiny-Q8_0.gguf \ +TRANSCRIBE_WHISPER_GGUF=$PWD/models/whisper-tiny/whisper-tiny-Q8_0.gguf \ ctest --test-dir build --output-on-failure -R whisper ``` diff --git a/docs/porting/families/cohere.md b/docs/porting/families/cohere.md index cb2b1402..d078566a 100644 --- a/docs/porting/families/cohere.md +++ b/docs/porting/families/cohere.md @@ -64,7 +64,7 @@ Real-model smokes: cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_COHERE_MODEL=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf \ +TRANSCRIBE_COHERE_GGUF=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf \ ctest --test-dir build --output-on-failure -R cohere ``` diff --git a/docs/porting/families/parakeet.md b/docs/porting/families/parakeet.md index 72f67ebb..1a70c958 100644 --- a/docs/porting/families/parakeet.md +++ b/docs/porting/families/parakeet.md @@ -96,7 +96,7 @@ Real-model smokes: cmake -B build -DTRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON cmake --build build -TRANSCRIBE_REAL_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v2/parakeet-tdt-0.6b-v2-F32.gguf \ +TRANSCRIBE_PARAKEET_GGUF=models/parakeet-tdt-0.6b-v2/parakeet-tdt-0.6b-v2-F32.gguf \ ctest --test-dir build --output-on-failure -R 'parakeet|encoder|decoder' ``` diff --git a/docs/porting/families/whisper.md b/docs/porting/families/whisper.md index 4bd41c8f..e7d2e9aa 100644 --- a/docs/porting/families/whisper.md +++ b/docs/porting/families/whisper.md @@ -209,7 +209,7 @@ See `reports/porting/whisper/whisper-tiny/intake.json::known_risks`. Highlights: These items are real C++ work for Stage 4 / 5 but do NOT flow through intake → convert → validate (the GGUF remains the canonical numerical reference): -- **C++ mel frontend (deferred).** Stage 4 bringup runs the encoder against the reference mel injected via `TRANSCRIBE_WHISPER_MEL_FROM_REF=`. `validate.py cpp` sets this env var automatically for the whisper family. The C++ mel frontend (slaney filterbank + Hann periodic window + whisper_logmel dynamic-range compression) is a follow-up. Tolerance file currently records `enc.mel.in` as zero-drift because we read the reference dump; the entry will need widening when the C++ frontend lands. +- **C++ mel frontend (deferred).** Stage 4 bringup runs the encoder against the reference mel injected via `TRANSCRIBE_MEL_FROM_REF=`. `validate.py cpp` sets this env var automatically for the whisper family. The C++ mel frontend (slaney filterbank + Hann periodic window + whisper_logmel dynamic-range compression) is a follow-up. Tolerance file currently records `enc.mel.in` as zero-drift because we read the reference dump; the entry will need widening when the C++ frontend lands. - **whisper.cpp `.bin` loader compatibility (deferred).** The plan calls for accepting upstream whisper.cpp `.bin` files alongside our GGUF. Not implemented in Stage 4; tracked for a later stage. - **KV-cached decoder (shipped).** The decoder runs through a KV-cached path: cross K/V are precomputed once from the encoder output into the cross cache; self K/V are written per-step into the self cache. The same graph builder handles both the prompt pass (`n_tokens > 1, n_past = 0`) and per-step generation (`n_tokens = 1, n_past = current`). Self- and cross-caches are F16 by default (flip with `--kv-type f32` for tighter parity). F16 cache introduces ~8× wider per-block `max_abs` drift than F32, concentrated in a few outlier elements; transcripts and 300-sample LibriSpeech WER are unchanged within CI. - **Beam search / temperature fallback (deferred).** Greedy argmax with `suppress_tokens` + first-step `begin_suppress_tokens` is enough to match the reference dumps' transcripts byte-for-byte under `do_sample=False, num_beams=1`. Beam search and temperature fallback (whisper.cpp's `temperature_inc` strategy) are follow-ups for the production decode path. diff --git a/docs/tools/README.md b/docs/tools/README.md index b1eb4cb3..5e3b8e47 100644 --- a/docs/tools/README.md +++ b/docs/tools/README.md @@ -62,6 +62,10 @@ This split mirrors `llama.cpp`'s `convert_hf_to_gguf.py` → ## Other +- [**environment-variables.md**](../environment-variables.md) — the single + reference for every env var the library, tests, and tooling recognize, split + into Tier 1 runtime config, Tier 2 validation hooks (build-gated), and + test/tooling. - `scripts/envs//pyproject.toml` — per-family `uv` env. Each converter and reference dumper has its own env because NeMo and Transformers have conflicting dependency graphs. diff --git a/reports/porting/medasr/forward-map.md b/reports/porting/medasr/forward-map.md index f096e405..c9ff5cc0 100644 --- a/reports/porting/medasr/forward-map.md +++ b/reports/porting/medasr/forward-map.md @@ -18,7 +18,7 @@ relative-position. The block is therefore hand-built in | Stage | Reference location | Output shape | Gate tensor | ggml / C++ pattern | In-tree analog | |-------|--------------------|--------------|-------------|--------------------|----------------| -| log-mel | `feature_extraction_lasr.LasrFeatureExtractor.__call__` | `[T_mel=floor((n_pcm-400)/160)+1, 128]` f32 | `mel.in` | host-side `transcribe::MelFrontend`; LASR knobs require `center=false`, `pad_mode="zero"`, `pre_emphasis=0.0`, `normalize="none"`, `window=ckpt`, `filterbank=ckpt`, `log_compression="log_clamp_1e-5"`. Step 2 brings up the encoder with `TRANSCRIBE_MEDASR_MEL_FROM_REF=` so Step 7 (frontend parity) is the first place the C++ mel actually runs. | `src/transcribe-mel.{h,cpp}` (extend with `center=false` knob and `log_compression` enum) | +| log-mel | `feature_extraction_lasr.LasrFeatureExtractor.__call__` | `[T_mel=floor((n_pcm-400)/160)+1, 128]` f32 | `mel.in` | host-side `transcribe::MelFrontend`; LASR knobs require `center=false`, `pad_mode="zero"`, `pre_emphasis=0.0`, `normalize="none"`, `window=ckpt`, `filterbank=ckpt`, `log_compression="log_clamp_1e-5"`. Step 2 brings up the encoder with `TRANSCRIBE_MEL_FROM_REF=` so Step 7 (frontend parity) is the first place the C++ mel actually runs. | `src/transcribe-mel.{h,cpp}` (extend with `center=false` knob and `log_compression` enum) | For jfk.wav (176 400 samples): T_mel = (176 400 − 400)/160 + 1 = 1 099 frames — reference dump rounds to 1 098 (one trailing frame dropped because LASR's manual unfold does `len // win` floor on the residual; not centered). The forward-map records the formula; bit-exact frame count is a Step 7 parity check. diff --git a/scripts/validate.py b/scripts/validate.py index c237aec3..32baa139 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -252,10 +252,29 @@ def find_cli(repo: Path) -> Path: return candidate raise SystemExit( "error: transcribe-cli not found in build/bin/\n" - " Run: cmake --build build --target transcribe-cli" + " Run: cmake --build build --target transcribe-cli\n" + " For --mel-from-ref and per-layer dumps, configure with\n" + " -DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON (or: cmake --preset validation)." ) +def validation_hooks_enabled(repo: Path) -> bool: + """True if build/ was configured with the validation hooks compiled in. + + Reads build/CMakeCache.txt next to the CLI that find_cli() runs. Used to + hard-fail --mel-from-ref against a build that would otherwise silently + ignore it (the hook code is compiled out unless the flag is ON). + """ + cache = repo / "build" / "CMakeCache.txt" + try: + for line in cache.read_text().splitlines(): + if line.startswith("TRANSCRIBE_ENABLE_VALIDATION_HOOKS:"): + return line.rstrip().endswith("=ON") + except OSError: + pass + return False + + def run_cmd(cmd: list[str], repo: Path, label: str) -> None: print(f"\n{'=' * 60}", file=sys.stderr) print(f" {label}", file=sys.stderr) @@ -420,8 +439,16 @@ def cmd_cpp(args: argparse.Namespace) -> int: # to ref-mel hid a base.en regression once (the mel-precision # change in 4613129); we don't want that blind spot back. if args.family == "whisper" and getattr(args, "mel_from_ref", False): + if not validation_hooks_enabled(repo): + raise SystemExit( + "error: --mel-from-ref requires validation hooks compiled " + "in, but build/ has TRANSCRIBE_ENABLE_VALIDATION_HOOKS=OFF " + "(the hook is compiled out and would be silently ignored).\n" + " Reconfigure: cmake --preset validation && " + "cmake --build build --target transcribe-cli" + ) ref_dir = repo / "build" / "validate" / args.family / variant / case_name / "ref" - env["TRANSCRIBE_WHISPER_MEL_FROM_REF"] = str(ref_dir) + env["TRANSCRIBE_MEL_FROM_REF"] = str(ref_dir) cmd = [ str(cli), @@ -650,7 +677,7 @@ def cmd_mel(args: argparse.Namespace) -> int: env = os.environ.copy() env["TRANSCRIBE_DUMP_DIR"] = str(out_dir) - env.pop("TRANSCRIBE_WHISPER_MEL_FROM_REF", None) + env.pop("TRANSCRIBE_MEL_FROM_REF", None) cmd = [ str(cli), @@ -872,10 +899,11 @@ def add_common(sp: argparse.ArgumentParser) -> None: ) sp_cpp.add_argument( "--mel-from-ref", action="store_true", - help="Whisper-only debug knob: inject the reference mel via " - "TRANSCRIBE_WHISPER_MEL_FROM_REF so enc.mel.in is bit-" + help="Whisper-only validation hook: inject the reference mel via " + "TRANSCRIBE_MEL_FROM_REF so enc.mel.in is bit-" "identical to HF's WhisperFeatureExtractor. Use to isolate " "graph drift from frontend drift when a regression fires. " + "Requires a build with -DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON. " "Default is the production C++ MelFrontend.", ) sp_cpp.set_defaults(func=cmd_cpp) @@ -912,9 +940,10 @@ def add_common(sp: argparse.ArgumentParser) -> None: ) sp_all.add_argument( "--mel-from-ref", action="store_true", - help="Whisper-only debug knob: inject the reference mel via " - "TRANSCRIBE_WHISPER_MEL_FROM_REF for the cpp dump. Default " - "is the production C++ MelFrontend.", + help="Whisper-only validation hook: inject the reference mel via " + "TRANSCRIBE_MEL_FROM_REF for the cpp dump. Requires a build with " + "-DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON. Default is the " + "production C++ MelFrontend.", ) sp_all.set_defaults(func=cmd_all) diff --git a/scripts/wer/remote/modal_sweep.py b/scripts/wer/remote/modal_sweep.py index ec85ffac..868d549c 100644 --- a/scripts/wer/remote/modal_sweep.py +++ b/scripts/wer/remote/modal_sweep.py @@ -239,6 +239,9 @@ def build(arch: str, build_dir: str, clean: bool = False) -> None: "-DCMAKE_BUILD_TYPE=Release", "-DTRANSCRIBE_CUDA=ON", "-DTRANSCRIBE_BUILD_TESTS=OFF", + # The sweep's validation/debug paths rely on the numerical-parity hooks + # (reference-mel injection + per-layer dumps), so compile them in here. + "-DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON", f"-DCMAKE_CUDA_ARCHITECTURES={arch}", "-DCMAKE_C_COMPILER_LAUNCHER=ccache", "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache", @@ -259,94 +262,6 @@ def build(arch: str, build_dir: str, clean: bool = False) -> None: # 2. Dataset prefetch (idempotent; cached on /data volume). # --------------------------------------------------------------------------- -@app.function( - image=image, timeout=1800, gpu="L4", - secrets=[modal.Secret.from_name("huggingface-secret")], - volumes={ - "/build": build_vol, - "/root/.cache/huggingface": hf_vol, - "/data": data_vol, - }, -) -def debug_dump(model_repo: str, model_file: str) -> dict: - """Run transcribe-cli once on jfk.wav with TRANSCRIBE_MEDASR_DEBUG_STATS - set, capturing stderr (where the encoder writes per-block tensor stats). - Returns the model_repo, model_file, and full stderr string so the local - caller can grep for the first divergence point between F16 and Q4_K_M. - """ - from huggingface_hub import hf_hub_download - prepare_work() - build_dir = _build_dir("L4") - build.remote(arch=GPU_TO_ARCH["L4"], build_dir=build_dir, clean=False) - # build runs in a different container that commits build_vol; our view - # of /build is stale until we reload the volume. - build_vol.reload() - cli_path = f"{build_dir}/bin/transcribe-cli" - assert os.path.exists(cli_path), f"missing CLI: {cli_path}" - model_path = hf_hub_download( - repo_id=model_repo, - filename=model_file, - token=os.environ["HF_TOKEN"], - ) - audio = "/work/samples/jfk.wav" - if not os.path.exists(audio): - # samples/** is excluded from the source mount (it holds the multi-GB - # LibriSpeech extract), so synthesize a short 16 kHz tone — enough to - # exercise model load + first compute, which is where load-time CUDA - # crashes surface. - import wave, struct, math - audio = "/tmp/_debug_tone.wav" - with wave.open(audio, "w") as w: - w.setnchannels(1); w.setsampwidth(2); w.setframerate(16000) - w.writeframes(b"".join( - struct.pack(" None: - """Local entrypoint: `modal run scripts/wer/remote/modal_sweep.py::dump_debug - --model-repo handy-computer/medasr-gguf --model-file medasr-Q4_K_M.gguf`. - Prints stderr lines that contain STATS so we can diff F16 vs Q4_K_M.""" - result = debug_dump.remote(model_repo, model_file) - print(f"=== {result['model_repo']} / {result['model_file']} (rc={result['rc']}) ===") - if result["rc"] != 0: - # Crash case (e.g. CUDA load/compute abort): the filtered STATS view - # would hide the actual error, so dump full stderr + stdout verbatim. - print("--- FULL STDERR (rc != 0) ---") - print(result["stderr"]) - print("--- FULL STDOUT (rc != 0) ---") - print(result["stdout"]) - return - for line in result["stderr"].splitlines(): - if "STATS " in line or "FAIL" in line.upper() or "ERR" in line.upper(): - print(line) - print(f"--- transcript from stdout ---") - for line in result["stdout"].splitlines(): - if line.startswith("text:"): - print(line) - - @app.function( image=image, timeout=3600, diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c9fd52b1..e8b9ef13 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -22,6 +22,7 @@ add_library(transcribe transcribe-unicode.cpp transcribe-unicode-data.cpp transcribe-debug.cpp + transcribe-env.cpp transcribe-flash-policy.cpp transcribe-backend.cpp transcribe-load-common.cpp @@ -150,6 +151,10 @@ if(TRANSCRIBE_GGML_BACKEND_DL) endif() endif() +if(TRANSCRIBE_ENABLE_VALIDATION_HOOKS) + target_compile_definitions(transcribe PRIVATE TRANSCRIBE_ENABLE_VALIDATION_HOOKS) +endif() + # Static posture: tell transcribe.h to drop the Windows __declspec(dllimport/ # export) decoration. PUBLIC so it reaches both this archive's own TUs and any # CMake consumer linking `transcribe` (C++ tests/examples) — without it a static diff --git a/src/arch/canary/encoder.cpp b/src/arch/canary/encoder.cpp index 62ca3c09..9385b00b 100644 --- a/src/arch/canary/encoder.cpp +++ b/src/arch/canary/encoder.cpp @@ -26,11 +26,9 @@ namespace conf = transcribe::conformer; // Direct dw on every backend for the in-block site; im2col for pre-encode // (canary's encoder geometry matches parakeet's at the dw site). bool detect_direct_dw_in_block(const char * /*backend*/) { - const char * env = std::getenv("TRANSCRIBE_CONV_DIRECT_DW"); - if (env != nullptr) return true; - env = std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW"); - if (env != nullptr) return false; - return true; + return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW", + "TRANSCRIBE_CONV_NO_DIRECT_DW", + /*backend_default=*/true); } conf::PreEncodeView to_view(const CanaryPreEncode & pe) { diff --git a/src/arch/canary/model.cpp b/src/arch/canary/model.cpp index 82f6fa50..f1848605 100644 --- a/src/arch/canary/model.cpp +++ b/src/arch/canary/model.cpp @@ -13,6 +13,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-flash-policy.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" @@ -1786,7 +1787,7 @@ transcribe_status run_batch( cc->batch_results.push_back(std::move(rs)); } - if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { + if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) { log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "canary run_batch: n=%d T_enc_max=%d kv_cap=%d prompt=%d\n" " mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)", diff --git a/src/arch/canary_qwen/canary_qwen.h b/src/arch/canary_qwen/canary_qwen.h index 8bdffbac..422b66a6 100644 --- a/src/arch/canary_qwen/canary_qwen.h +++ b/src/arch/canary_qwen/canary_qwen.h @@ -114,8 +114,8 @@ struct CanaryQwenSession final : public transcribe_session { std::vector enc_host; // perception output [hidden=2048, T_enc] // Reference (NeMo SALM) ran flash off; we follow suit by default for - // tightest tensor parity. Override with TRANSCRIBE_FLASH_DECODER=1 - // (or =encoder) at runtime. + // tightest tensor parity. Override with TRANSCRIBE_NO_FLASH / + // TRANSCRIBE_FORCE_FLASH (both stages) at runtime. bool encoder_use_flash = false; bool decoder_use_flash = false; diff --git a/src/arch/canary_qwen/encoder.cpp b/src/arch/canary_qwen/encoder.cpp index ff0cc785..4eee347f 100644 --- a/src/arch/canary_qwen/encoder.cpp +++ b/src/arch/canary_qwen/encoder.cpp @@ -32,9 +32,9 @@ namespace { namespace conf = transcribe::conformer; bool detect_direct_dw_in_block(const char * /*backend*/) { - if (std::getenv("TRANSCRIBE_CONV_DIRECT_DW") != nullptr) return true; - if (std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW") != nullptr) return false; - return true; + return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW", + "TRANSCRIBE_CONV_NO_DIRECT_DW", + /*backend_default=*/true); } conf::PreEncodeView to_view(const PreEncodeSlots & pe) { diff --git a/src/arch/canary_qwen/model.cpp b/src/arch/canary_qwen/model.cpp index df856f60..1d8cd953 100644 --- a/src/arch/canary_qwen/model.cpp +++ b/src/arch/canary_qwen/model.cpp @@ -27,6 +27,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-flash-policy.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" @@ -716,7 +717,7 @@ transcribe_status init_context( // Flash defaults: encoder off (the FastConformer rel-pos path has a manual // rel_shift trick ggml_flash_attn_ext doesn't subsume), decoder ON (~2.4x // decode speedup on jfk.wav, the Qwen3 step graph is dispatch-bound on - // Metal). TRANSCRIBE_FLASH_DECODER=1 (or =encoder) overrides. + // Metal). TRANSCRIBE_NO_FLASH / TRANSCRIBE_FORCE_FLASH override both stages. cc->encoder_use_flash = false; cc->decoder_use_flash = true; transcribe::flash::apply_env_overrides(cc->encoder_use_flash, @@ -1057,7 +1058,7 @@ transcribe_status run(transcribe_session * context, 0, mask.size() * sizeof(ggml_fp16_t)); } - const bool profile_decode = (std::getenv("TRANSCRIBE_PROFILE_DECODE") != nullptr); + const bool profile_decode = transcribe::env::flag("TRANSCRIBE_PERF_DEBUG"); int64_t t_prefill_us = 0; int64_t t_prefill_compute_us = 0; { diff --git a/src/arch/cohere/encoder.cpp b/src/arch/cohere/encoder.cpp index ecec71ff..a3911892 100644 --- a/src/arch/cohere/encoder.cpp +++ b/src/arch/cohere/encoder.cpp @@ -29,14 +29,12 @@ namespace conf = transcribe::conformer; // (native kernels), im2col + mul_mat on Metal/CPU. See the ConvPolicy // comment in conformer.h for why the two dw sites share one detect result. bool detect_direct_dw(const char * backend) { - const char * env = std::getenv("TRANSCRIBE_CONV_DIRECT_DW"); - if (env != nullptr) return true; - env = std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW"); - if (env != nullptr) return false; - if (backend == nullptr) return false; - if (std::strstr(backend, "Vulkan") != nullptr) return true; - if (std::strstr(backend, "CUDA") != nullptr) return true; - return false; + const bool backend_default = backend != nullptr && + (std::strstr(backend, "Vulkan") != nullptr || + std::strstr(backend, "CUDA") != nullptr); + return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW", + "TRANSCRIBE_CONV_NO_DIRECT_DW", + backend_default); } // ----- Views ------------------------------------------------------ diff --git a/src/arch/cohere/model.cpp b/src/arch/cohere/model.cpp index 039fadf2..bf8cb262 100644 --- a/src/arch/cohere/model.cpp +++ b/src/arch/cohere/model.cpp @@ -14,6 +14,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-flash-policy.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" @@ -1806,7 +1807,7 @@ transcribe_status run_batch( cc->batch_results.push_back(std::move(rs)); } - if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { + if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) { log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "cohere run_batch: n=%d T_enc_max=%d kv_cap=%d prompt=%d\n" " mel=%.1fms (parallel) enc=%.1fms (serial x%d) decode=%.1fms (batched)", diff --git a/src/arch/gigaam/model.cpp b/src/arch/gigaam/model.cpp index 9e0b2bd6..7b8a441c 100644 --- a/src/arch/gigaam/model.cpp +++ b/src/arch/gigaam/model.cpp @@ -13,6 +13,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" #include "transcribe-log.h" @@ -209,6 +210,7 @@ namespace { // ne=[T_mel, n_mels] (fast=T_mel, slow=n_mels), and ggml's // fast-to-slow layout maps byte i to ne_index (t=i%T_mel, m=i/T_mel). // Both byte orders are identical — just a memcpy is needed. +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS transcribe_status load_ref_mel(const std::string & dir, int n_mels, int & n_mel_frames, @@ -247,6 +249,7 @@ transcribe_status load_ref_mel(const std::string & dir, } return TRANSCRIBE_OK; } +#endif // TRANSCRIBE_ENABLE_VALIDATION_HOOKS // Host-side RNN-T / CTC greedy decode of one utterance's encoder slice, // publishing the transcript into the session scratch slot (full_text / @@ -370,14 +373,17 @@ transcribe_status run(transcribe_session * session, // center=False + periodic Hann). The env-var injection is a debug knob // for tensor-parity isolation. int mel_n_frames = 0; + bool mel_from_ref = false; const int64_t t_mel_start = ggml_time_us(); - if (const char * ref_dir = std::getenv("TRANSCRIBE_GIGAAM_MEL_FROM_REF"); - ref_dir != nullptr && ref_dir[0] != '\0') - { +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS + if (const char * ref_dir = transcribe::env::str("TRANSCRIBE_MEL_FROM_REF")) { if (auto st = load_ref_mel(ref_dir, gm->hparams.fe_num_mels, mel_n_frames, gc->mel_buf); st != TRANSCRIBE_OK) return st; - } else { + mel_from_ref = true; + } +#endif + if (!mel_from_ref) { if (auto st = gm->mel.compute(pcm, static_cast(n_samples), gc->mel_buf, mel_n_frames); st != TRANSCRIBE_OK) return st; @@ -674,7 +680,7 @@ transcribe_status run_batch_encode(GigaamSession * gc, // Debug-only full [d, T, n] intermediates, gated on the env var. if (transcribe::debug::enabled() && - std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr) + transcribe::debug::dump_all_blocks_requested()) { if (eb.dumps.pre_encode_out != nullptr) { transcribe::debug::dump_tensor("enc.subsample.out", diff --git a/src/arch/granite/model.cpp b/src/arch/granite/model.cpp index 0c808279..597d792e 100644 --- a/src/arch/granite/model.cpp +++ b/src/arch/granite/model.cpp @@ -10,6 +10,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-flash-policy.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" @@ -1625,7 +1626,7 @@ transcribe_status run_batch( } const int64_t step_us = step_stats.step_us; - if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { + if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) { int total_steps = 0; for (int b = 0; b < n; ++b) total_steps = std::max(total_steps, static_cast(generated[b].size())); diff --git a/src/arch/medasr/encoder.cpp b/src/arch/medasr/encoder.cpp index 858f911f..d8395d84 100644 --- a/src/arch/medasr/encoder.cpp +++ b/src/arch/medasr/encoder.cpp @@ -15,6 +15,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-flash-policy.h" #include "transcribe-log.h" #include "ggml.h" @@ -503,10 +504,13 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, eb.bn_bias_inputs[i] = bt; } - // Flash attention defaults on. Forced off when the manual SDPA path - // is required (variable-length key-padding mask, single-shot/same- - // length batches keep flash on). - const bool use_flash = (std::getenv("TRANSCRIBE_NO_FLASH") == nullptr); + // Flash attention defaults on. Forced off when the manual SDPA path is + // required (variable-length key-padding mask, single-shot/same-length + // batches keep flash on). TRANSCRIBE_NO_FLASH / TRANSCRIBE_FORCE_FLASH + // override; MedASR is CTC-only so the decoder flag is a throwaway. + bool enc_use_flash = true, dec_use_flash = false; + transcribe::flash::apply_env_overrides(enc_use_flash, dec_use_flash); + const bool use_flash = enc_use_flash; eb.dumps.all_block_outs.assign(n_layers, nullptr); diff --git a/src/arch/medasr/model.cpp b/src/arch/medasr/model.cpp index 2ffc3c8e..cb48be61 100644 --- a/src/arch/medasr/model.cpp +++ b/src/arch/medasr/model.cpp @@ -7,7 +7,7 @@ // init_context() allocates the per-context state. // // run() builds the encoder graph, uploads the mel (or the reference -// mel via TRANSCRIBE_MEDASR_MEL_FROM_REF), allocates and uploads the +// mel via TRANSCRIBE_MEL_FROM_REF), allocates and uploads the // per-block fused-BN scale/bias inputs, computes the graph, reads // back the CTC logits, performs greedy collapse (drop blanks, collapse // repeats), and populates the result snapshot. @@ -20,6 +20,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" #include "transcribe-log.h" @@ -299,6 +300,7 @@ transcribe_status init_context(transcribe_model * model, return TRANSCRIBE_OK; } +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS // Load reference mel sidecar from /mel.in.f32 (or // frontend.mel.out.f32). transcribe_status load_ref_mel(const std::string & dir, @@ -349,6 +351,7 @@ transcribe_status load_ref_mel(const std::string & dir, } return TRANSCRIBE_OK; } +#endif // TRANSCRIBE_ENABLE_VALIDATION_HOOKS // Greedy CTC decode: argmax per frame, collapse repeats, drop blanks // AND skip the LASR special tokens (=1, =2, =3) — matching @@ -402,14 +405,17 @@ transcribe_status run(transcribe_session * session, // ----- Mel acquisition ----------------------------------------------- int mel_n_frames = 0; + bool mel_from_ref = false; const int64_t t_mel_start = ggml_time_us(); - if (const char * ref_dir = std::getenv("TRANSCRIBE_MEDASR_MEL_FROM_REF"); - ref_dir != nullptr && ref_dir[0] != '\0') - { +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS + if (const char * ref_dir = transcribe::env::str("TRANSCRIBE_MEL_FROM_REF")) { if (auto st = load_ref_mel(ref_dir, gm->hparams.fe_num_mels, mel_n_frames, gc->mel_buf); st != TRANSCRIBE_OK) return st; - } else { + mel_from_ref = true; + } +#endif + if (!mel_from_ref) { int out_n_mels = 0; std::vector m_major; if (auto st = gm->mel->compute(pcm, static_cast(n_samples), @@ -578,54 +584,6 @@ transcribe_status run(transcribe_session * session, try_dump("enc.out_norm.out", eb.dumps.out_norm_out, "encoder.out_norm"); try_dump("enc.ctc_logits", eb.dumps.ctc_logits_for_dump, "encoder.ctc_logits"); - // Diagnostic: per-block stats gated by TRANSCRIBE_MEDASR_DEBUG_STATS. - // Prints min / max / mean / has_nan / has_inf for each marker tensor - // to stderr (Modal captures and surfaces stderr in the run logs). - // Used to localize where CUDA Q* output diverges from F32 / F16. - if (const char * dbg = std::getenv("TRANSCRIBE_MEDASR_DEBUG_STATS"); - dbg != nullptr && dbg[0] != '\0') - { - std::vector tmp; - auto report = [&](const char * name, ggml_tensor * t) { - if (t == nullptr) return; - const size_t n = static_cast(ggml_nelements(t)); - tmp.assign(n, 0.0f); - ggml_backend_tensor_get(t, tmp.data(), 0, n * sizeof(float)); - float mn = +1e30f, mx = -1e30f; - double sum = 0.0; - int n_nan = 0, n_inf = 0; - for (size_t i = 0; i < n; ++i) { - const float v = tmp[i]; - if (std::isnan(v)) { ++n_nan; continue; } - if (std::isinf(v)) { ++n_inf; continue; } - sum += v; - if (v < mn) mn = v; - if (v > mx) mx = v; - } - const double mean = n > 0 ? sum / static_cast(n) : 0.0; - log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, - "STATS %-26s n=%zu min=%+.4e max=%+.4e mean=%+.4e nan=%d inf=%d", - name, n, mn, mx, mean, n_nan, n_inf); - }; - report("mel.in", eb.mel_in); - report("sub.after_dense0", eb.dumps.sub_after_dense0); - report("sub.after_conv0", eb.dumps.sub_after_conv0); - report("sub.after_conv1", eb.dumps.sub_after_conv1); - report("enc.subsampling.out", eb.dumps.subsampling_out); - report("enc.block.0.post_ff1", eb.dumps.block0_post_ff1); - report("enc.block.0.post_attn", eb.dumps.block0_post_attn); - report("enc.block.0.post_conv", eb.dumps.block0_post_conv); - report("enc.block.0.post_ff2", eb.dumps.block0_post_ff2); - for (int i = 0; i < n_layers; ++i) { - if (eb.dumps.all_block_outs[i] == nullptr) continue; - char nm[32]; - std::snprintf(nm, sizeof(nm), "enc.block.%d.out", i); - report(nm, eb.dumps.all_block_outs[i]); - } - report("enc.out_norm.out", eb.dumps.out_norm_out); - report("enc.ctc_logits", eb.dumps.ctc_logits_for_dump); - } - // Per-utterance encoder-output dump (single-shot baseline for the // batched-equals-single-shot gate). Matches the parakeet / gigaam name. if (transcribe::debug::enabled() && eb.dumps.out_norm_out != nullptr) { diff --git a/src/arch/moonshine/model.cpp b/src/arch/moonshine/model.cpp index 8accd8dc..dde59f6b 100644 --- a/src/arch/moonshine/model.cpp +++ b/src/arch/moonshine/model.cpp @@ -14,6 +14,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-flash-policy.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" @@ -1120,7 +1121,7 @@ transcribe_status run_batch( cc->batch_results.push_back(std::move(rs)); } - if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { + if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) { log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "moonshine run_batch: n=%d T_enc_max=%d max_n_kv=%d\n" " enc=%.1fms (serial x%d) decode=%.1fms (batched)", diff --git a/src/arch/moonshine_streaming/model.cpp b/src/arch/moonshine_streaming/model.cpp index 2eaad940..3c6983e1 100644 --- a/src/arch/moonshine_streaming/model.cpp +++ b/src/arch/moonshine_streaming/model.cpp @@ -31,6 +31,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-flash-policy.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" @@ -2232,7 +2233,7 @@ transcribe_status run_batch( cc->batch_results.push_back(std::move(rs)); } - if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { + if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) { log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "moonshine_streaming run_batch: n=%d T_enc_max=%d kv_window=%d (cap %d)\n" " enc+adapter=%.1fms (serial x%d) decode=%.1fms (batched)", diff --git a/src/arch/parakeet/encoder.cpp b/src/arch/parakeet/encoder.cpp index 4d680b24..0d9b4b64 100644 --- a/src/arch/parakeet/encoder.cpp +++ b/src/arch/parakeet/encoder.cpp @@ -26,6 +26,7 @@ #include "conformer/conformer.h" #include "transcribe-debug.h" +#include "transcribe-flash-policy.h" #include "transcribe-log.h" #include "ggml.h" @@ -49,12 +50,10 @@ namespace conf = transcribe::conformer; // backend (faster than the 31x im2col expansion). Env vars override // both sites. bool detect_direct_dw_in_block(const char * backend) { - const char * env = std::getenv("TRANSCRIBE_CONV_DIRECT_DW"); - if (env != nullptr) return true; // user override - env = std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW"); - if (env != nullptr) return false; // user override (void)backend; - return true; + return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW", + "TRANSCRIBE_CONV_NO_DIRECT_DW", + /*backend_default=*/true); } // Pre-encode depthwise dispatch: direct path EXCEPT on Metal, where @@ -63,13 +62,12 @@ bool detect_direct_dw_in_block(const char * backend) { // CONV_2D_DW; the direct path avoids the ~31x im2col expansion and the // degenerate per-channel [1 x K] matmul (~40-50ms here). bool detect_direct_dw_in_pre_encode(const char * backend) { - if (std::getenv("TRANSCRIBE_CONV_DIRECT_DW") != nullptr) return true; - if (std::getenv("TRANSCRIBE_CONV_NO_DIRECT_DW") != nullptr) return false; - if (backend != nullptr && - (std::strstr(backend, "Metal") != nullptr || std::strstr(backend, "metal") != nullptr)) { - return false; - } - return true; + const bool is_metal = backend != nullptr && + (std::strstr(backend, "Metal") != nullptr || + std::strstr(backend, "metal") != nullptr); + return conf::resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_DW", + "TRANSCRIBE_CONV_NO_DIRECT_DW", + /*backend_default=*/!is_metal); } // ----- Views ------------------------------------------------------ @@ -216,7 +214,7 @@ void parakeet_subblock_observer_cb(void * user, std::vector parse_sub_block_env(int n_layers) { std::vector out; - const char * raw = std::getenv("TRANSCRIBE_DUMP_SUB_BLOCKS"); + const char * raw = transcribe::debug::dump_sub_blocks_spec(); if (raw == nullptr || *raw == '\0') return out; const char * p = raw; while (*p != '\0') { @@ -431,10 +429,14 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, bparams.kv_type = kv_type; // Flash attention by default. Flash batches correctly (the batched // encoder output is bit-identical to single-shot flash), so the batch - // axis does not change the path. TRANSCRIBE_NO_FLASH=1 forces the - // manual F32 mul_mat + soft_max path — used by the bit-exact CPU - // tensor gate (flash casts the rel-pos mask to F16, manual stays F32). - bparams.use_flash = (std::getenv("TRANSCRIBE_NO_FLASH") == nullptr); + // axis does not change the path. TRANSCRIBE_NO_FLASH forces the manual + // F32 mul_mat + soft_max path — used by the bit-exact CPU tensor gate + // (flash casts the rel-pos mask to F16, manual stays F32); + // TRANSCRIBE_FORCE_FLASH forces it back on. Parakeet has no attention + // decoder, so the decoder flag is a throwaway. + bool enc_use_flash = true, dec_use_flash = false; + transcribe::flash::apply_env_overrides(enc_use_flash, dec_use_flash); + bparams.use_flash = enc_use_flash; bparams.policy = policy; bparams.att_context_left = hp.enc_att_context_left; bparams.att_context_right = hp.enc_att_context_right; @@ -476,7 +478,7 @@ EncoderBuild build_encoder_graph(ggml_context * ctx, // Per-block dump opt-in for the divergence bisect // (TRANSCRIBE_DUMP_ALL_BLOCKS). const bool dump_all_blocks = - std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr; + transcribe::debug::dump_all_blocks_requested(); if (dump_all_blocks && eb.dumps.block0_out != nullptr) { transcribe::debug::mark_tensor_for_dump(eb.dumps.block0_out); } @@ -756,10 +758,13 @@ EncoderBuild build_encoder_graph_streaming( bparams.n_head = hp.enc_n_heads; bparams.conv_kernel = hp.enc_conv_kernel; bparams.kv_type = kv_type; - // Flash-attention opt-out (TRANSCRIBE_NO_FLASH). On CPU at streaming - // geometry the flash kernel is dramatically slower (~33 ms/layer vs - // ~1 ms on a Cortex-A55). - bparams.use_flash = (std::getenv("TRANSCRIBE_NO_FLASH") == nullptr); + // Flash-attention opt-out (TRANSCRIBE_NO_FLASH / TRANSCRIBE_FORCE_FLASH). + // On CPU at streaming geometry the flash kernel is dramatically slower + // (~33 ms/layer vs ~1 ms on a Cortex-A55). Parakeet has no attention + // decoder, so the decoder flag is a throwaway. + bool enc_use_flash = true, dec_use_flash = false; + transcribe::flash::apply_env_overrides(enc_use_flash, dec_use_flash); + bparams.use_flash = enc_use_flash; bparams.policy = policy; // No att_context_left/right: ChunkedLimited derives the band entirely // from attn_chunked_mask (build_conformer_block reads them only on the diff --git a/src/arch/parakeet/model.cpp b/src/arch/parakeet/model.cpp index 9c34cd5d..02d14914 100644 --- a/src/arch/parakeet/model.cpp +++ b/src/arch/parakeet/model.cpp @@ -1213,7 +1213,7 @@ static transcribe_status run_one_shot_inner( } // Per-block dump for the layer-by-layer divergence bisect (gated by // TRANSCRIBE_DUMP_ALL_BLOCKS). - if (std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr) { + if (transcribe::debug::dump_all_blocks_requested()) { for (size_t i = 0; i < eb.dumps.all_block_outs.size(); ++i) { ggml_tensor * t = eb.dumps.all_block_outs[i]; if (t == nullptr) continue; @@ -1531,7 +1531,7 @@ static transcribe_status run_batch_encode( // [d_model, T_max, n] tensors, so a batched-vs-single divergence can be // located per stage. Gated on TRANSCRIBE_DUMP_ALL_BLOCKS. if (transcribe::debug::enabled() && - std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr) + transcribe::debug::dump_all_blocks_requested()) { if (eb.dumps.pre_encode_out != nullptr) { transcribe::debug::dump_tensor("enc.pre_encode.out", @@ -2547,7 +2547,7 @@ transcribe_status emit_buffered_chunk( transcribe::debug::dump_tensor(name, eb.dumps.last_block_out, "encoder.block.last.out"); } - if (std::getenv("TRANSCRIBE_DUMP_ALL_BLOCKS") != nullptr) { + if (transcribe::debug::dump_all_blocks_requested()) { for (size_t i = 0; i < eb.dumps.all_block_outs.size(); ++i) { ggml_tensor * t = eb.dumps.all_block_outs[i]; if (t == nullptr) continue; diff --git a/src/arch/qwen3_asr/model.cpp b/src/arch/qwen3_asr/model.cpp index cd1e632d..2b55d245 100644 --- a/src/arch/qwen3_asr/model.cpp +++ b/src/arch/qwen3_asr/model.cpp @@ -10,6 +10,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-flash-policy.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" @@ -1143,8 +1144,7 @@ transcribe_status run( // Optional perf breakdown (finer split than the public mel/encode/decode // timings), gated on env var. - if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); - e != nullptr && *e != '\0' && *e != '0') + if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) { const double ms = 1.0 / 1000.0; const double sum_ms = (cc->t_mel_us + t_enc_build_us + @@ -1790,8 +1790,7 @@ transcribe_status run_batch( cc->batch_results.push_back(std::move(rs)); } - if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); - e != nullptr && *e != '\0' && *e != '0') { + if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) { log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "qwen3_asr run_batch: n=%d valid=%d max_n_kv=%d steps=%d (all phases batched x%d)\n" " enc_pass=%.1fms (mel=%.1f parallel + enc_compute=%.1f, 1 graph)\n" diff --git a/src/arch/voxtral_realtime/model.cpp b/src/arch/voxtral_realtime/model.cpp index f05414a7..799d4720 100644 --- a/src/arch/voxtral_realtime/model.cpp +++ b/src/arch/voxtral_realtime/model.cpp @@ -17,6 +17,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-flash-policy.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" @@ -99,6 +100,7 @@ void apply_threads(ggml_backend_sched_t sched, int n_threads) { transcribe::configure_sched_n_threads(sched, n_threads); } +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS // Read a reference enc.mel.in.f32 ([n_mels, n_frames] mel-major) for numerical // isolation. Returns true and fills mel_buf (mel-major) + n_frames on success. bool read_ref_mel(const std::string & dir, int n_mels, @@ -118,6 +120,7 @@ bool read_ref_mel(const std::string & dir, int n_mels, n_frames = static_cast(n / n_mels); return true; } +#endif // TRANSCRIBE_ENABLE_VALIDATION_HOOKS // Input-length contract (see docs/input-limits.md). Chunked/unbounded family: // the encoder is causal + sliding-window (750 frames) advanced on a @@ -434,14 +437,18 @@ transcribe_status forward_buffer(Session * cc, Model * cm, // ----- Mel (or reference-injected mel for numerical isolation) ----- const int n_mels = cm->hparams.enc_num_mel_bins; int mel_n_frames = 0; - const char * ref_mel_dir = std::getenv("TRANSCRIBE_VOXTRAL_REALTIME_MEL_FROM_REF"); + bool mel_from_ref = false; const int64_t t_mel_start = ggml_time_us(); - if (ref_mel_dir != nullptr && ref_mel_dir[0] != '\0') { +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS + if (const char * ref_mel_dir = transcribe::env::str("TRANSCRIBE_MEL_FROM_REF")) { if (!read_ref_mel(ref_mel_dir, n_mels, cc->mel_buf, mel_n_frames)) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "voxtral_realtime run: failed to read ref mel from %s", ref_mel_dir); return TRANSCRIBE_ERR_GGUF; } - } else { + mel_from_ref = true; + } +#endif + if (!mel_from_ref) { // Streaming offline audio padding (mistral-common encode_transcription, // StreamingMode.OFFLINE): pad the audio into the 12.5 Hz token grid as // [n_left_pad zeros] + [raw rounded up to raw_per_tok] + [n_right zeros] @@ -1534,7 +1541,7 @@ transcribe_status stream_process(Session * cc, Model * cm, bool is_final, bool * } // Per-component timing (diagnostic): printed once at finalize. - if (is_final && std::getenv("TRANSCRIBE_VOXTRAL_REALTIME_STREAM_TIMING") != nullptr) { + if (is_final && transcribe::env::flag("TRANSCRIBE_VOXTRAL_REALTIME_STREAM_TIMING")) { const double mel = cc->stream_t_mel_us / 1000.0, conv = cc->stream_t_conv_us / 1000.0; const double enc = cc->stream_t_enc_us / 1000.0, dec = cc->stream_t_dec_us / 1000.0; const double enc_c = cc->stream_t_enc_compute_us / 1000.0; // pure graph_compute diff --git a/src/arch/whisper/model.cpp b/src/arch/whisper/model.cpp index 94f7c1ab..f0479077 100644 --- a/src/arch/whisper/model.cpp +++ b/src/arch/whisper/model.cpp @@ -1,7 +1,7 @@ // arch/whisper/model.cpp - Whisper ASR family handler. // // Normal inference uses the C++ mel frontend and the autoregressive decoder -// path below; TRANSCRIBE_WHISPER_MEL_FROM_REF can inject a reference mel tensor +// path below; TRANSCRIBE_MEL_FROM_REF can inject a reference mel tensor // to isolate encoder/decoder drift during numerical validation. #include "whisper.h" @@ -13,6 +13,7 @@ #include "transcribe-arch.h" #include "transcribe-batch-util.h" #include "transcribe-debug.h" +#include "transcribe-env.h" #include "transcribe-flash-policy.h" #include "transcribe-load-common.h" #include "transcribe-loader.h" @@ -305,7 +306,7 @@ bool ensure_compute_ctx(WhisperSession * cc, size_t mem) { } // Print the per-stage performance summary collected during the most -// recent whisper_run. Opt-in via TRANSCRIBE_WHISPER_PROFILE=1 — gated +// recent whisper_run. Opt-in via TRANSCRIBE_PERF_DEBUG — gated // by the caller, this helper just formats the counters. Output is one // summary line per stage (encoder / cross / prompt / step) plus a // per-step average for the steady-state generation loop, which is the @@ -372,11 +373,11 @@ void print_whisper_perf(const WhisperPerf & p) { avg_us(p.step_compute), avg_us(p.step_tensor_get), avg_us(p.step_cpu)); - // CPU sub-section breakdown — opt-in via TRANSCRIBE_WHISPER_PROFILE - // values that contain "cpu" or "all". Keeps the default profile - // output unchanged for existing benchmarks while still surfacing + // CPU sub-section breakdown — opt-in via TRANSCRIBE_PERF_DEBUG values + // that contain "cpu" or "all". Keeps the default profile output + // unchanged for existing benchmarks while still surfacing // suppress/timestamp/sample/logprob splits when needed. - const char * profile_env = std::getenv("TRANSCRIBE_WHISPER_PROFILE"); + const char * profile_env = transcribe::env::str("TRANSCRIBE_PERF_DEBUG"); bool show_cpu_breakdown = false; if (profile_env != nullptr) { const std::string v = profile_env; @@ -409,8 +410,7 @@ void print_whisper_perf(const WhisperPerf & p) { } bool whisper_perf_enabled() { - const char * s = std::getenv("TRANSCRIBE_WHISPER_PROFILE"); - return s != nullptr && s[0] != '\0' && s[0] != '0'; + return transcribe::env::flag("TRANSCRIBE_PERF_DEBUG"); } // load @@ -990,6 +990,7 @@ float logprob_of_token_hf( return logits[token_id] * rescale_T - log_Z; } +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS // Load 3000 * 80 f32 floats from /enc.mel.in.f32. Layout on disk // matches the reference dump: row-major (3000, 80) — exactly what we // need to upload into the ggml ne=[80, 3000] input tensor via a flat @@ -1029,6 +1030,7 @@ transcribe_status load_mel_from_ref(const char * ref_dir, } return TRANSCRIBE_OK; } +#endif // TRANSCRIBE_ENABLE_VALIDATION_HOOKS } // namespace @@ -1290,9 +1292,10 @@ transcribe_status whisper_run( // ----- Mel frontend ----- // - // 1. TRANSCRIBE_WHISPER_MEL_FROM_REF= — debug knob that reads the - // reference mel dump from disk so encoder drift can be isolated from - // C++ mel drift. The default path exercises the production frontend. + // 1. TRANSCRIBE_MEL_FROM_REF= — validation hook (compiled in only + // with -DTRANSCRIBE_ENABLE_VALIDATION_HOOKS) that reads the reference + // mel dump from disk so encoder drift can be isolated from C++ mel + // drift. The default path exercises the production frontend. // // 2. C++ MelFrontend (default): // - Short-form (n_samples <= fe_n_samples): pad PCM to fe_n_samples @@ -1312,9 +1315,9 @@ transcribe_status whisper_run( const int64_t t_mel_start = ggml_time_us(); int total_mel_frames = 0; - if (const char * ref_dir = std::getenv("TRANSCRIBE_WHISPER_MEL_FROM_REF"); - ref_dir != nullptr && ref_dir[0] != '\0') - { + bool mel_from_ref = false; +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS + if (const char * ref_dir = transcribe::env::str("TRANSCRIBE_MEL_FROM_REF")) { if (const transcribe_status st = load_mel_from_ref( ref_dir, n_mels, n_mel_frames_per_chunk, cc->mel_buf); st != TRANSCRIBE_OK) @@ -1322,7 +1325,10 @@ transcribe_status whisper_run( return st; } total_mel_frames = n_mel_frames_per_chunk; - } else { + mel_from_ref = true; + } +#endif + if (!mel_from_ref) { if (!cm->mel.has_value()) { log_msg(TRANSCRIBE_LOG_LEVEL_ERROR, "whisper run: model has no MelFrontend " @@ -3341,7 +3347,7 @@ transcribe_status whisper_run_batch( have_result[b] = 1; } - if (const char * e = std::getenv("TRANSCRIBE_PERF_DEBUG"); e && *e && *e != '0') { + if (transcribe::env::flag("TRANSCRIBE_PERF_DEBUG")) { int n_serial = 0; for (int b = 0; b < n; ++b) n_serial += needs_serial[b] ? 1 : 0; log_msg(TRANSCRIBE_LOG_LEVEL_DEBUG, "whisper run_batch: n=%d batched=%d serial=%d tiers=%d want_ts=%d " diff --git a/src/arch/whisper/whisper.h b/src/arch/whisper/whisper.h index 3686cce2..4546fbfa 100644 --- a/src/arch/whisper/whisper.h +++ b/src/arch/whisper/whisper.h @@ -178,7 +178,7 @@ bool kv_cache_init_batched(WhisperKvCache & cache, ggml_type kv_type); // Per-stage timing counters. Always-on (timestamps are negligible); printing -// is opt-in via TRANSCRIBE_WHISPER_PROFILE=1. Reset at the top of whisper_run. +// is opt-in via TRANSCRIBE_PERF_DEBUG. Reset at the top of whisper_run. struct WhisperPerfStage { int64_t total_us = 0; int count = 0; @@ -220,7 +220,7 @@ struct WhisperPerf { WhisperPerfStage step_cpu; // CPU-section sub-counters. Always populated (the timestamps are - // negligible) but only printed when TRANSCRIBE_WHISPER_PROFILE + // negligible) but only printed when TRANSCRIBE_PERF_DEBUG // includes "cpu" or "all". Splits the prompt_cpu / step_cpu // total into the four sub-stages of post-graph processing. WhisperPerfStage prompt_cpu_suppress; @@ -313,7 +313,7 @@ struct WhisperSession final : public transcribe_session { // at the top of each run alongside cc->clear_result(). std::vector chunk_traces; - // Per-stage timing counters; summary printed when TRANSCRIBE_WHISPER_PROFILE=1. + // Per-stage timing counters; summary printed when TRANSCRIBE_PERF_DEBUG is set. WhisperPerf perf; // Reusable scratch for the multinomial T>0 sampler, sized to vocab_size on diff --git a/src/conformer/conformer.cpp b/src/conformer/conformer.cpp index 8f533eca..26722912 100644 --- a/src/conformer/conformer.cpp +++ b/src/conformer/conformer.cpp @@ -3,9 +3,12 @@ // See conformer.h for the API contract. Each family's encoder.cpp keeps // only the glue binding these helpers to its weights / hparams / graph // driver; per-family policy knobs (ConvPolicy, BlockParams) are passed in. -// No environment variables are read here except by detect_direct_pw. +// Conv-dispatch env vars (TRANSCRIBE_CONV_DIRECT_PW/DW and their NO_ variants) +// are read only via resolve_conv_direct here, which detect_direct_pw and every +// family's depthwise detect delegate to — one place owns the parsing. #include "conformer/conformer.h" +#include "transcribe-env.h" #include "transcribe-log.h" #include "ggml.h" @@ -309,18 +312,23 @@ ggml_tensor * add_conv_bias(ggml_context * ctx, return ggml_add(ctx, conv_out, bias_4d); } +bool resolve_conv_direct(const char * direct_env, + const char * no_direct_env, + bool backend_default) { + if (transcribe::env::flag(direct_env)) return true; // user override + if (transcribe::env::flag(no_direct_env)) return false; // user override + return backend_default; +} + bool detect_direct_pw(const char * backend) { - const char * env = std::getenv("TRANSCRIBE_CONV_NO_DIRECT_PW"); - if (env != nullptr) return false; // user override - env = std::getenv("TRANSCRIBE_CONV_DIRECT_PW"); - if (env != nullptr) return true; // user override // Vulkan defaults to im2col: on AMD Renoir, im2col + mul_mat measured // ~200 ms/encode faster than direct for f32 weights. Metal/CPU prefer // direct. - if (backend != nullptr && std::strstr(backend, "Vulkan") != nullptr) { - return false; - } - return true; + const bool backend_default = + !(backend != nullptr && std::strstr(backend, "Vulkan") != nullptr); + return resolve_conv_direct("TRANSCRIBE_CONV_DIRECT_PW", + "TRANSCRIBE_CONV_NO_DIRECT_PW", + backend_default); } // Conv module (pointwise -> GLU -> depthwise -> BN/LN -> SiLU -> pointwise). diff --git a/src/conformer/conformer.h b/src/conformer/conformer.h index 59c80b69..8e9b891a 100644 --- a/src/conformer/conformer.h +++ b/src/conformer/conformer.h @@ -123,9 +123,19 @@ struct ConvPolicy { bool causal_pre_encode = false; }; +// Resolve a conv-dispatch toggle from its env overrides. The DIRECT var forces +// true, the NO_DIRECT var forces false (DIRECT wins if both are set); otherwise +// `backend_default` is used. Centralizes the override parsing shared by the +// pointwise and the per-family depthwise dispatch helpers, so the env-var read +// has exactly one definition. `direct_env` / `no_direct_env` are env var names +// (e.g. "TRANSCRIBE_CONV_DIRECT_DW" / "TRANSCRIBE_CONV_NO_DIRECT_DW"). +bool resolve_conv_direct(const char * direct_env, + const char * no_direct_env, + bool backend_default); + // Pointwise conv dispatch auto-detect (same answer for every family today). -// Env overrides: TRANSCRIBE_CONV_NO_DIRECT_PW=1 forces im2col, -// TRANSCRIBE_CONV_DIRECT_PW=1 forces direct. Vulkan defaults to im2col: on +// Env overrides: TRANSCRIBE_CONV_NO_DIRECT_PW forces im2col, +// TRANSCRIBE_CONV_DIRECT_PW forces direct. Vulkan defaults to im2col: on // AMD Renoir, im2col + mul_mat measured ~200 ms/encode faster than direct for // f32 weights. Metal and CPU prefer direct. bool detect_direct_pw(const char * backend); diff --git a/src/transcribe-debug.cpp b/src/transcribe-debug.cpp index c1d21aa7..30f92a3d 100644 --- a/src/transcribe-debug.cpp +++ b/src/transcribe-debug.cpp @@ -8,6 +8,8 @@ #include "transcribe-debug.h" +#include "transcribe-env.h" + #include "ggml.h" #include "ggml-backend.h" @@ -136,6 +138,30 @@ const char * dump_dir() { return enabled() ? g_dump_dir.c_str() : nullptr; } +bool validation_hooks_enabled() { +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS + return true; +#else + return false; +#endif +} + +bool dump_all_blocks_requested() { +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS + return transcribe::env::flag("TRANSCRIBE_DUMP_ALL_BLOCKS"); +#else + return false; +#endif +} + +const char * dump_sub_blocks_spec() { +#ifdef TRANSCRIBE_ENABLE_VALIDATION_HOOKS + return transcribe::env::str("TRANSCRIBE_DUMP_SUB_BLOCKS"); +#else + return nullptr; +#endif +} + void push_name_prefix(const char * prefix) { if (!enabled() || prefix == nullptr) return; g_name_prefix_stack.emplace_back(prefix); diff --git a/src/transcribe-debug.h b/src/transcribe-debug.h index f10befc2..79c6f8a2 100644 --- a/src/transcribe-debug.h +++ b/src/transcribe-debug.h @@ -42,6 +42,28 @@ bool enabled(); // as the dumper is enabled. Useful for tests. const char * dump_dir(); +// Validation-hook gating ------------------------------------------------------ +// +// The reference-mel injection (TRANSCRIBE_MEL_FROM_REF) and the per-layer +// tensor dumps (TRANSCRIBE_DUMP_ALL_BLOCKS / TRANSCRIBE_DUMP_SUB_BLOCKS) are +// numerical-parity hooks for porting/validation. They are compiled in only when +// the library is built with -DTRANSCRIBE_ENABLE_VALIDATION_HOOKS=ON (see the +// CMake option). Release builds leave them out. + +// True if the library was compiled with validation hooks enabled. +bool validation_hooks_enabled(); + +// Per-layer block dump opt-in (TRANSCRIBE_DUMP_ALL_BLOCKS). Always false unless +// the library was built with validation hooks. The env var name lives only in +// transcribe-debug.cpp (behind the compile guard) so it is absent from the +// strings of a release binary. +bool dump_all_blocks_requested(); + +// TRANSCRIBE_DUMP_SUB_BLOCKS spec (CSV of block indices to dump sub-layer +// activations for) or nullptr. Always nullptr unless built with validation +// hooks. +const char * dump_sub_blocks_spec(); + // Push/pop a name prefix that gets prepended to every subsequent // dump_tensor / dump_host_f32 call. Used by buffered streaming to // scope per-chunk intermediate dumps (e.g. "stream.chunk.5.") so the diff --git a/src/transcribe-env.cpp b/src/transcribe-env.cpp new file mode 100644 index 00000000..a1d4a618 --- /dev/null +++ b/src/transcribe-env.cpp @@ -0,0 +1,22 @@ +// transcribe-env.cpp - see transcribe-env.h. + +#include "transcribe-env.h" + +#include + +namespace transcribe::env { + +const char * str(const char * name) { + const char * v = std::getenv(name); + if (v == nullptr || v[0] == '\0') { + return nullptr; + } + return v; +} + +bool flag(const char * name) { + const char * v = str(name); + return v != nullptr && v[0] != '0'; +} + +} // namespace transcribe::env diff --git a/src/transcribe-env.h b/src/transcribe-env.h new file mode 100644 index 00000000..5bdd56cb --- /dev/null +++ b/src/transcribe-env.h @@ -0,0 +1,24 @@ +// transcribe-env.h - tiny helpers for reading environment variables with one +// consistent parse convention across the codebase. +// +// INTERNAL, C++17. Not part of the public ABI. +// +// A "flag" env var is considered ON when it is set, non-empty, and its first +// character is not '0' (so FOO=1 / FOO=on / FOO=yes are ON; FOO=0 and FOO= +// and unset are OFF). This matches the dominant pre-existing convention +// (TRANSCRIBE_PERF_DEBUG) and is the single source of truth for boolean env +// toggles — do not hand-roll getenv() comparisons elsewhere. + +#pragma once + +namespace transcribe::env { + +// True iff `name` is set, non-empty, and its first character is not '0'. +bool flag(const char * name); + +// The value of `name`, or nullptr if it is unset or empty. The returned +// pointer is owned by the environment (do not free); valid until the next +// setenv/putenv. +const char * str(const char * name); + +} // namespace transcribe::env diff --git a/src/transcribe-flash-policy.cpp b/src/transcribe-flash-policy.cpp index d269c9b5..168f835f 100644 --- a/src/transcribe-flash-policy.cpp +++ b/src/transcribe-flash-policy.cpp @@ -4,19 +4,17 @@ #include "transcribe-flash-policy.h" -#include +#include "transcribe-env.h" namespace transcribe::flash { void apply_env_overrides(bool & encoder_use_flash, bool & decoder_use_flash) { - const char * no_flash = std::getenv("TRANSCRIBE_NO_FLASH"); - if (no_flash != nullptr && no_flash[0] == '1') { + if (transcribe::env::flag("TRANSCRIBE_NO_FLASH")) { encoder_use_flash = false; decoder_use_flash = false; } - const char * force_flash = std::getenv("TRANSCRIBE_FORCE_FLASH"); - if (force_flash != nullptr && force_flash[0] == '1') { + if (transcribe::env::flag("TRANSCRIBE_FORCE_FLASH")) { encoder_use_flash = true; decoder_use_flash = true; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 64039f46..abbabde3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -489,7 +489,7 @@ endif() # # Gated two ways: by the CMake option above (so the binary is not # built unless the developer opts in) AND by the runtime -# TRANSCRIBE_REAL_PARAKEET_GGUF env var (so a configured-but-no-file +# TRANSCRIBE_PARAKEET_GGUF env var (so a configured-but-no-file # build still skips the test cleanly via SKIP_RETURN_CODE 77 instead # of failing the suite). @@ -524,7 +524,7 @@ endif() # # Same gating as the other real-model tests: built only when # TRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON; skipped at run time when -# TRANSCRIBE_REAL_PARAKEET_GGUF is unset. The reference text is +# TRANSCRIBE_PARAKEET_GGUF is unset. The reference text is # baked into the test source, not a golden file on disk, so the # test is hermetic — it does not need a golden generation step. @@ -727,7 +727,7 @@ endif() # # Same gating as the Parakeet real-model tests: built only when # TRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON. At run time, -# TRANSCRIBE_COHERE_MODEL must point at a Cohere ASR GGUF. Exits 77 +# TRANSCRIBE_COHERE_GGUF must point at a Cohere ASR GGUF. Exits 77 # (CTest "skipped") when the env var is unset or the file is missing. if(TRANSCRIBE_BUILD_REAL_MODEL_TESTS) @@ -841,7 +841,7 @@ endif() # # Gated two ways: by TRANSCRIBE_BUILD_REAL_MODEL_TESTS (so the binary # is not built unless the developer opts in) AND by the runtime -# TRANSCRIBE_COHERE_MODEL env var (same var that cohere_e2e_smoke.cpp +# TRANSCRIBE_COHERE_GGUF env var (same var that cohere_e2e_smoke.cpp # uses, so a single export drives both tests). Skipped via # SKIP_RETURN_CODE 77 when the env var is unset or the file is # missing. diff --git a/tests/cohere_e2e_smoke.cpp b/tests/cohere_e2e_smoke.cpp index 6b66ad08..13cf54d3 100644 --- a/tests/cohere_e2e_smoke.cpp +++ b/tests/cohere_e2e_smoke.cpp @@ -20,7 +20,7 @@ // // - The CMake option TRANSCRIBE_BUILD_REAL_MODEL_TESTS (default OFF) // controls whether this binary is built. -// - At runtime, the GGUF path comes from TRANSCRIBE_COHERE_MODEL +// - At runtime, the GGUF path comes from TRANSCRIBE_COHERE_GGUF // env var. If unset, the test exits 77 (CTest "skipped"). // Numerical accuracy validation lives in validate.py; this // test covers API behavior and structural correctness only. @@ -140,10 +140,10 @@ int main() { // ---- Resolve model path ------------------------------------------ std::string model_path; { - const char * env = std::getenv("TRANSCRIBE_COHERE_MODEL"); + const char * env = std::getenv("TRANSCRIBE_COHERE_GGUF"); if (env == nullptr || env[0] == '\0') { std::fprintf(stderr, - "cohere_e2e_smoke: TRANSCRIBE_COHERE_MODEL not set. " + "cohere_e2e_smoke: TRANSCRIBE_COHERE_GGUF not set. " "Skipping.\n"); return 77; // CTest "skipped" } @@ -153,7 +153,7 @@ int main() { if (!file_exists(model_path)) { std::fprintf(stderr, "cohere_e2e_smoke: model not found: %s\n" - "Set TRANSCRIBE_COHERE_MODEL to a valid GGUF path. " + "Set TRANSCRIBE_COHERE_GGUF to a valid GGUF path. " "Skipping.\n", model_path.c_str()); return 77; // CTest "skipped" diff --git a/tests/cohere_real_smoke.cpp b/tests/cohere_real_smoke.cpp index 06874e95..0f62e556 100644 --- a/tests/cohere_real_smoke.cpp +++ b/tests/cohere_real_smoke.cpp @@ -11,7 +11,7 @@ // - The CMake option TRANSCRIBE_BUILD_REAL_MODEL_TESTS (default OFF) // controls whether this binary is even built. // - At runtime, the GGUF path comes from the -// TRANSCRIBE_COHERE_MODEL environment variable (same var that +// TRANSCRIBE_COHERE_GGUF environment variable (same var that // cohere_smoke.cpp uses, so a single export drives both tests). // If unset, the test exits 77 (CTest "skipped") with a // regeneration hint. @@ -120,14 +120,14 @@ cohere_view(const struct transcribe_model * m) { } // namespace int main() { - const char * env = std::getenv("TRANSCRIBE_COHERE_MODEL"); + const char * env = std::getenv("TRANSCRIBE_COHERE_GGUF"); if (env == nullptr || env[0] == '\0') { std::fprintf(stderr, - "cohere_real_smoke: TRANSCRIBE_COHERE_MODEL not " + "cohere_real_smoke: TRANSCRIBE_COHERE_GGUF not " "set; skipping. Convert a real model with:\n" " uv run scripts/convert-cohere.py " " --repo-id CohereLabs/cohere-transcribe-03-2026\n" - "and re-run with TRANSCRIBE_COHERE_MODEL=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf\n"); + "and re-run with TRANSCRIBE_COHERE_GGUF=models/cohere-transcribe-03-2026/cohere-transcribe-03-2026-BF16.gguf\n"); return 77; } const std::string fixture = env; diff --git a/tests/decoder_smoke.cpp b/tests/decoder_smoke.cpp index 407a2aa4..d993ad80 100644 --- a/tests/decoder_smoke.cpp +++ b/tests/decoder_smoke.cpp @@ -26,7 +26,7 @@ // validation. // // Gating: built only when TRANSCRIBE_BUILD_REAL_MODEL_TESTS=ON; at run -// time TRANSCRIBE_REAL_PARAKEET_GGUF must point at a v2 GGUF. The test +// time TRANSCRIBE_PARAKEET_GGUF must point at a v2 GGUF. The test // exits 77 (CTest "skipped") when the model path is unset or missing. // // The reference text was validated against v2; v3's encoder weights are @@ -118,10 +118,10 @@ constexpr int k_max_edit_distance = 3; int main() { // ---- Resolve env ----------------------------------------------- - const char * gguf_env = std::getenv("TRANSCRIBE_REAL_PARAKEET_GGUF"); + const char * gguf_env = std::getenv("TRANSCRIBE_PARAKEET_GGUF"); if (gguf_env == nullptr || gguf_env[0] == '\0') { std::fprintf(stderr, - "decoder_smoke: TRANSCRIBE_REAL_PARAKEET_GGUF not set; " + "decoder_smoke: TRANSCRIBE_PARAKEET_GGUF not set; " "skipping\n"); return 77; } diff --git a/tests/parakeet_real_smoke.cpp b/tests/parakeet_real_smoke.cpp index 3a9e6303..d2a351a8 100644 --- a/tests/parakeet_real_smoke.cpp +++ b/tests/parakeet_real_smoke.cpp @@ -10,7 +10,7 @@ // - The CMake option TRANSCRIBE_BUILD_REAL_MODEL_TESTS (default OFF) // controls whether this binary is even built. // - At runtime, the GGUF path comes from the -// TRANSCRIBE_REAL_PARAKEET_GGUF environment variable. If unset, +// TRANSCRIBE_PARAKEET_GGUF environment variable. If unset, // the test exits 77 (CTest "skipped") with a regeneration hint. // // CI never builds this — it's a developer-local manual gate. The @@ -117,14 +117,14 @@ parakeet_view(const struct transcribe_model * m) { } // namespace int main() { - const char * env = std::getenv("TRANSCRIBE_REAL_PARAKEET_GGUF"); + const char * env = std::getenv("TRANSCRIBE_PARAKEET_GGUF"); if (env == nullptr || env[0] == '\0') { std::fprintf(stderr, - "parakeet_real_smoke: TRANSCRIBE_REAL_PARAKEET_GGUF not " + "parakeet_real_smoke: TRANSCRIBE_PARAKEET_GGUF not " "set; skipping. Convert a real model with:\n" " uv run scripts/convert-parakeet.py " " /tmp/parakeet.gguf\n" - "and re-run with TRANSCRIBE_REAL_PARAKEET_GGUF=/tmp/parakeet.gguf\n"); + "and re-run with TRANSCRIBE_PARAKEET_GGUF=/tmp/parakeet.gguf\n"); return 77; } const std::string fixture = env; diff --git a/tests/whisper_e2e_smoke.cpp b/tests/whisper_e2e_smoke.cpp index 16a157e9..9505f3ce 100644 --- a/tests/whisper_e2e_smoke.cpp +++ b/tests/whisper_e2e_smoke.cpp @@ -59,10 +59,10 @@ bool file_exists(const std::string & path) { } // namespace int main() { - const char * env = std::getenv("TRANSCRIBE_WHISPER_MODEL"); + const char * env = std::getenv("TRANSCRIBE_WHISPER_GGUF"); if (env == nullptr || env[0] == '\0') { std::fprintf(stderr, - "whisper_e2e_smoke: TRANSCRIBE_WHISPER_MODEL not set; skipping.\n"); + "whisper_e2e_smoke: TRANSCRIBE_WHISPER_GGUF not set; skipping.\n"); return 77; } const std::string model_path = env; diff --git a/tests/whisper_tokenize_parity.cpp b/tests/whisper_tokenize_parity.cpp index 5e05eb3f..764d2629 100644 --- a/tests/whisper_tokenize_parity.cpp +++ b/tests/whisper_tokenize_parity.cpp @@ -12,7 +12,7 @@ // but the text-only BPE pieces tested here sit in the shared prefix // of both vocabs, so the expected ids match there too. // -// Gated by TRANSCRIBE_WHISPER_MODEL (same env var used by +// Gated by TRANSCRIBE_WHISPER_GGUF (same env var used by // whisper_e2e_smoke). Exits 77 (cmake SKIP_RETURN_CODE) when unset. #include "transcribe.h" @@ -73,10 +73,10 @@ void check_case(const struct transcribe_model * model, const Case & c) { } // namespace int main() { - const char * env = std::getenv("TRANSCRIBE_WHISPER_MODEL"); + const char * env = std::getenv("TRANSCRIBE_WHISPER_GGUF"); if (env == nullptr || env[0] == '\0') { std::fprintf(stderr, - "whisper_tokenize_parity: TRANSCRIBE_WHISPER_MODEL " + "whisper_tokenize_parity: TRANSCRIBE_WHISPER_GGUF " "not set; skipping.\n"); return 77; }