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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions .claude/skills/porting-3-convert/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Convert progress:
- [ ] Step 2: Identify reference dtype from intake
- [ ] Step 3: Run the converter (reference dtype only)
- [ ] Step 4: Write the converter manifest
- [ ] Step 5: Run structural check (Preflight Gate B)
- [ ] Step 5: Run structural checks (Preflight Gate B + loader smoke + quant-policy sync)
- [ ] Step 6: Sign-off review
```

Expand All @@ -41,6 +41,34 @@ Preserve source dtypes, emit the loader KV used by
`src/arch/<family>/weights.cpp`, and surface only unresolved tensor-name
or sharding decisions to the user.

**Always build the writer via `gguf_writer()` from `lib.gguf_common`**, never
`gguf.GGUFWriter` directly:

```python
from lib.gguf_common import gguf_writer
writer = gguf_writer(str(out_path), "<arch>")
```

`gguf_writer()` automatically relocates the bulk tokenizer KVs
(`tokenizer.ggml.tokens` / `scores` / `token_type` / `merges`,
`tokenizer.chat_template`) to a trailer after all scalar metadata, so remote
consumers can range-read the small metadata prefix without pulling the multi-MB
tokenizer tables.

**Per-tensor dtype bucketing.** Converters choose each tensor's storage dtype via
`reference_dtype_for()` from `lib.gguf_common` (biases / norm scales / positional
tables / frontend buffers → F32; conv kernels → F16 when the reference dtype is
BF16, which the loader has no conv kernel for; everything else keeps the reference
dtype). This is a Python mirror of the canonical bucketing in
`tools/transcribe-quantize/policy.cpp::classify_tensor`, which the Stage 5
quantizer uses. The two are hand-synced, so when this family needs a tensor kept
out of the reference dtype — a new norm/conv/positional name the loader requires
at F32/F16 — add the rule to **both** `reference_dtype_for` **and**
`policy.cpp::classify_tensor`, then add a representative tensor name for this
family to the corpus in `scripts/lib/test_quant_policy_sync.py`. That test
(Step 5) is what keeps the two copies from drifting; catching it here, at convert
time, avoids a wrong-dtype surprise when Stage 5 quantizes.

### Step 2: Identify reference dtype (read intake)

```bash
Expand Down Expand Up @@ -125,6 +153,14 @@ build/bin/transcribe-cli -m models/<variant>/<variant>-<REFDTYPE>.gguf samples/j

For a brand-new family where `src/arch/<family>/` doesn't exist yet, the loader returns `TRANSCRIBE_ERR_UNSUPPORTED_ARCH`. That is acceptable at Stage 3 — note it in sign-off; Stage 4 (`porting-4-cpp`) brings up the arch. For an established family the smoke must exit 0. A per-family real-model smoke (`tests/<family>_real_smoke.cpp`) is a Stage 4 artifact, not a Stage 3 gate.

Quant-policy sync: the converter-side dtype bucketing (`reference_dtype_for`) must
stay aligned with the canonical `policy.cpp::classify_tensor` and must not have
regressed. Fast, no model files:

```bash
uv run scripts/lib/test_quant_policy_sync.py
```

### Step 6: Sign-off

Report:
Expand All @@ -141,6 +177,7 @@ Report:
- `models/<variant>/<variant>-<REFDTYPE>.gguf` exists.
- `reports/convert/<variant>-<REFDTYPE>.json` exists and records the SHA + source revision.
- Preflight Gate B is green.
- `scripts/lib/test_quant_policy_sync.py` exits 0 (converter-side dtype bucketing in sync with `policy.cpp`).
- The full quant matrix is NOT generated here — that is Stage 5 (`porting-5-quants`).

## Pointers (read, not execute)
Expand All @@ -151,5 +188,6 @@ Report:
- `scripts/convert-parakeet.py` (NeMo `.nemo`)
- `scripts/convert-cohere.py` (Transformers)
- `scripts/convert-qwen3_asr.py` (author-repo)
- `scripts/lib/gguf_common.py` — shared KV-write helpers (execute only indirectly, via the converter)
- `scripts/lib/gguf_common.py` — shared KV-write helpers + `reference_dtype_for` bucketing (execute only indirectly, via the converter)
- `scripts/lib/test_quant_policy_sync.py` — pins `reference_dtype_for` against `policy.cpp`; the Step 5 quant-policy gate, and where you register a new family's norm/conv tensor names
- `src/arch/<family>/weights.cpp` for any already-ported family — the loader's authoritative tensor-name and shape expectations
4 changes: 4 additions & 0 deletions .github/workflows/python-bindings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ on:
- "bindings/python/**"
- "include/**"
- "src/**"
- "scripts/lib/**"
- "tests/**"
- "ggml/**"
- "CMakeLists.txt"
Expand All @@ -36,6 +37,7 @@ on:
- "bindings/python/**"
- "include/**"
- "src/**"
- "scripts/lib/**"
- "tests/**"
- "ggml/**"
- "CMakeLists.txt"
Expand Down Expand Up @@ -76,6 +78,8 @@ jobs:
bindings/python/_generate/generate.py --check
- name: Version sync (header / pyproject / __init__)
run: uv run --no-project bindings/python/_generate/check_version_sync.py
- name: GGUF writer layout
run: uv run scripts/lib/test_gguf_writer.py

python-shared:
runs-on: blacksmith-2vcpu-ubuntu-2404
Expand Down
6 changes: 6 additions & 0 deletions bindings/python/src/transcribe_cpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,7 @@ class Capabilities:
supports_streaming: bool
supports_spec_decode: bool
max_audio_ms: int
translate_target_languages: tuple[str, ...]


@dataclass(frozen=True)
Expand Down Expand Up @@ -847,6 +848,10 @@ def capabilities(self) -> Capabilities:
if caps.languages and caps.n_languages > 0:
for i in range(caps.n_languages):
languages.append(_decode(caps.languages[i]))
translate_targets = []
if caps.translate_target_languages and caps.n_translate_target_languages > 0:
for i in range(caps.n_translate_target_languages):
translate_targets.append(_decode(caps.translate_target_languages[i]))
return Capabilities(
native_sample_rate=caps.native_sample_rate,
languages=tuple(languages),
Expand All @@ -856,6 +861,7 @@ def capabilities(self) -> Capabilities:
supports_streaming=bool(caps.supports_streaming),
supports_spec_decode=bool(caps.supports_spec_decode),
max_audio_ms=caps.max_audio_ms,
translate_target_languages=tuple(translate_targets),
)

def supports(self, feature: Feature) -> bool:
Expand Down
8 changes: 5 additions & 3 deletions bindings/python/src/transcribe_cpp/_generated.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
# Stable digest of the ABI surface below (structs, enums, macros, layout,
# prototypes). A native provider package echoes this back so the API
# package can reject an ABI-mismatched provider before dlopen.
PUBLIC_HEADER_HASH = "ebe6a6816e34a24e"
PUBLIC_HEADER_HASH = "86b16dd97ad1cb58"

# === enum constants ===
TRANSCRIBE_OK = 0
Expand Down Expand Up @@ -153,7 +153,7 @@ class transcribe_whisper_chunk_trace(_c.Structure):
transcribe_model_load_params._fields_ = [("struct_size", _c.c_uint64), ("backend", _c.c_int), ("gpu_device", _c.c_int)]
transcribe_session_params._fields_ = [("struct_size", _c.c_uint64), ("n_threads", _c.c_int), ("kv_type", _c.c_int), ("n_ctx", _c.c_int32)]
transcribe_run_params._fields_ = [("struct_size", _c.c_uint64), ("task", _c.c_int), ("timestamps", _c.c_int), ("pnc", _c.c_int), ("itn", _c.c_int), ("language", _c.c_char_p), ("target_language", _c.c_char_p), ("keep_special_tags", _c.c_bool), ("family", _c.POINTER(transcribe_ext)), ("spec_k_drafts", _c.c_int32)]
transcribe_capabilities._fields_ = [("struct_size", _c.c_uint64), ("native_sample_rate", _c.c_int32), ("n_languages", _c.c_int), ("languages", _c.POINTER(_c.c_char_p)), ("max_timestamp_kind", _c.c_int), ("supports_language_detect", _c.c_bool), ("supports_translate", _c.c_bool), ("supports_streaming", _c.c_bool), ("supports_spec_decode", _c.c_bool), ("max_audio_ms", _c.c_int64)]
transcribe_capabilities._fields_ = [("struct_size", _c.c_uint64), ("native_sample_rate", _c.c_int32), ("n_languages", _c.c_int), ("languages", _c.POINTER(_c.c_char_p)), ("max_timestamp_kind", _c.c_int), ("supports_language_detect", _c.c_bool), ("supports_translate", _c.c_bool), ("supports_streaming", _c.c_bool), ("supports_spec_decode", _c.c_bool), ("max_audio_ms", _c.c_int64), ("n_translate_target_languages", _c.c_int), ("translate_target_languages", _c.POINTER(_c.c_char_p))]
transcribe_session_limits._fields_ = [("struct_size", _c.c_uint64), ("effective_n_ctx", _c.c_int32), ("effective_max_audio_ms", _c.c_int64), ("max_kv_bytes", _c.c_int64)]
transcribe_stream_params._fields_ = [("struct_size", _c.c_uint64), ("family", _c.POINTER(transcribe_ext)), ("commit_policy", _c.c_int), ("stable_prefix_agreement_n", _c.c_uint32)]
transcribe_stream_update._fields_ = [("struct_size", _c.c_uint64), ("result_changed", _c.c_bool), ("is_final", _c.c_bool), ("revision", _c.c_int32), ("input_received_ms", _c.c_int64), ("audio_committed_ms", _c.c_int64), ("buffered_ms", _c.c_int64), ("committed_changed", _c.c_bool), ("tentative_changed", _c.c_bool)]
Expand Down Expand Up @@ -195,7 +195,7 @@ class transcribe_whisper_chunk_trace(_c.Structure):
'transcribe_model_load_params': {'size': 16, 'align': 8, 'offsets': {'struct_size': 0, 'backend': 8, 'gpu_device': 12}},
'transcribe_session_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'n_threads': 8, 'kv_type': 12, 'n_ctx': 16}},
'transcribe_run_params': {'size': 64, 'align': 8, 'offsets': {'struct_size': 0, 'task': 8, 'timestamps': 12, 'pnc': 16, 'itn': 20, 'language': 24, 'target_language': 32, 'keep_special_tags': 40, 'family': 48, 'spec_k_drafts': 56}},
'transcribe_capabilities': {'size': 40, 'align': 8, 'offsets': {'struct_size': 0, 'native_sample_rate': 8, 'n_languages': 12, 'languages': 16, 'max_timestamp_kind': 24, 'supports_language_detect': 28, 'supports_translate': 29, 'supports_streaming': 30, 'supports_spec_decode': 31, 'max_audio_ms': 32}},
'transcribe_capabilities': {'size': 56, 'align': 8, 'offsets': {'struct_size': 0, 'native_sample_rate': 8, 'n_languages': 12, 'languages': 16, 'max_timestamp_kind': 24, 'supports_language_detect': 28, 'supports_translate': 29, 'supports_streaming': 30, 'supports_spec_decode': 31, 'max_audio_ms': 32, 'n_translate_target_languages': 40, 'translate_target_languages': 48}},
'transcribe_session_limits': {'size': 32, 'align': 8, 'offsets': {'struct_size': 0, 'effective_n_ctx': 8, 'effective_max_audio_ms': 16, 'max_kv_bytes': 24}},
'transcribe_stream_params': {'size': 24, 'align': 8, 'offsets': {'struct_size': 0, 'family': 8, 'commit_policy': 16, 'stable_prefix_agreement_n': 20}},
'transcribe_stream_update': {'size': 48, 'align': 8, 'offsets': {'struct_size': 0, 'result_changed': 8, 'is_final': 9, 'revision': 12, 'input_received_ms': 16, 'audio_committed_ms': 24, 'buffered_ms': 32, 'committed_changed': 40, 'tentative_changed': 41}},
Expand Down Expand Up @@ -297,6 +297,8 @@ def configure(lib):
lib.transcribe_model_load_file.argtypes = [_c.c_char_p, _c.POINTER(transcribe_model_load_params), _c.POINTER(_c.c_void_p)]
lib.transcribe_model_load_params_init.restype = None
lib.transcribe_model_load_params_init.argtypes = [_c.POINTER(transcribe_model_load_params)]
lib.transcribe_model_meta_val_str.restype = _c.c_char_p
lib.transcribe_model_meta_val_str.argtypes = [_c.c_void_p, _c.c_char_p]
lib.transcribe_model_supports.restype = _c.c_bool
lib.transcribe_model_supports.argtypes = [_c.c_void_p, _c.c_int]
lib.transcribe_model_variant_string.restype = _c.c_char_p
Expand Down
18 changes: 15 additions & 3 deletions bindings/rust/sys/src/transcribe_sys.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// @generated by `cargo xtask bindgen` from include/transcribe/extensions.h
// DO NOT EDIT BY HAND. Regenerate: `cargo xtask bindgen`.
// Pinned to include/transcribe.abihash = ebe6a6816e34a24e
// Pinned to include/transcribe.abihash = 86b16dd97ad1cb58

/// The public-ABI digest these bindings were generated against
/// (sha256/16 over the normalized FFI surface). The load-time version
/// gate and the CI drift check both anchor on this value.
pub const PUBLIC_HEADER_HASH: &str = "ebe6a6816e34a24e";
pub const PUBLIC_HEADER_HASH: &str = "86b16dd97ad1cb58";

/* automatically generated by rust-bindgen 0.72.1 */

Expand Down Expand Up @@ -372,10 +372,12 @@ pub struct transcribe_capabilities {
pub supports_streaming: bool,
pub supports_spec_decode: bool,
pub max_audio_ms: i64,
pub n_translate_target_languages: ::std::os::raw::c_int,
pub translate_target_languages: *const *const ::std::os::raw::c_char,
}
#[allow(clippy::unnecessary_operation, clippy::identity_op)]
const _: () = {
["Size of transcribe_capabilities"][::std::mem::size_of::<transcribe_capabilities>() - 40usize];
["Size of transcribe_capabilities"][::std::mem::size_of::<transcribe_capabilities>() - 56usize];
["Alignment of transcribe_capabilities"]
[::std::mem::align_of::<transcribe_capabilities>() - 8usize];
["Offset of field: transcribe_capabilities::struct_size"]
Expand All @@ -398,6 +400,10 @@ const _: () = {
[::std::mem::offset_of!(transcribe_capabilities, supports_spec_decode) - 31usize];
["Offset of field: transcribe_capabilities::max_audio_ms"]
[::std::mem::offset_of!(transcribe_capabilities, max_audio_ms) - 32usize];
["Offset of field: transcribe_capabilities::n_translate_target_languages"]
[::std::mem::offset_of!(transcribe_capabilities, n_translate_target_languages) - 40usize];
["Offset of field: transcribe_capabilities::translate_target_languages"]
[::std::mem::offset_of!(transcribe_capabilities, translate_target_languages) - 48usize];
};
unsafe extern "C" {
pub fn transcribe_capabilities_init(out: *mut transcribe_capabilities);
Expand Down Expand Up @@ -440,6 +446,12 @@ unsafe extern "C" {
model: *const transcribe_model,
) -> *const ::std::os::raw::c_char;
}
unsafe extern "C" {
pub fn transcribe_model_meta_val_str(
model: *const transcribe_model,
key: *const ::std::os::raw::c_char,
) -> *const ::std::os::raw::c_char;
}
unsafe extern "C" {
pub fn transcribe_model_load_file(
path: *const ::std::os::raw::c_char,
Expand Down
15 changes: 15 additions & 0 deletions bindings/rust/transcribe-cpp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub struct Capabilities {
pub native_sample_rate: i32,
/// Supported language codes (empty if the model is language-agnostic).
pub languages: Vec<String>,
/// Supported translation target language codes (empty if not advertised).
pub translate_target_languages: Vec<String>,
/// The finest timestamp granularity the model can produce.
pub max_timestamp_kind: TimestampKind,
pub supports_language_detect: bool,
Expand Down Expand Up @@ -159,10 +161,23 @@ impl Model {
languages.push(owned_str(lang));
}
}
let mut translate_target_languages = Vec::new();
if !caps.translate_target_languages.is_null() && caps.n_translate_target_languages > 0 {
let slice = unsafe {
std::slice::from_raw_parts(
caps.translate_target_languages,
caps.n_translate_target_languages as usize,
)
};
for &lang in slice {
translate_target_languages.push(owned_str(lang));
}
}

Capabilities {
native_sample_rate: caps.native_sample_rate,
languages,
translate_target_languages,
max_timestamp_kind: TimestampKind::from_raw(caps.max_timestamp_kind),
supports_language_detect: caps.supports_language_detect,
supports_translate: caps.supports_translate,
Expand Down
2 changes: 1 addition & 1 deletion bindings/swift/Sources/TranscribeCpp/ABIHash.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import CTranscribe
extension Transcribe {
/// sha256/16 of the normalized public FFI surface, pinned to the value in
/// include/transcribe.abihash at the time this binding was last reviewed.
public static let pinnedHeaderHash = "ebe6a6816e34a24e"
public static let pinnedHeaderHash = "86b16dd97ad1cb58"

/// The public-ABI digest this binding was reviewed against (16 hex chars).
public static func headerHash() -> String { pinnedHeaderHash }
Expand Down
9 changes: 9 additions & 0 deletions bindings/swift/Sources/TranscribeCpp/Transcript.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ public struct Capabilities: Sendable, Equatable {
public let nativeSampleRate: Int32
/// Supported language codes; empty when the model is language-agnostic.
public let languages: [String]
/// Supported translation target language codes; empty when not advertised.
public let translateTargetLanguages: [String]
public let maxTimestampKind: TimestampKind
public let supportsLanguageDetect: Bool
public let supportsTranslate: Bool
Expand All @@ -105,6 +107,13 @@ public struct Capabilities: Sendable, Equatable {
}
}
languages = langs
var targetLangs: [String] = []
if let arr = c.translate_target_languages {
for i in 0..<Int(c.n_translate_target_languages) {
if let s = arr[i] { targetLangs.append(String(cString: s)) }
}
}
translateTargetLanguages = targetLangs
maxTimestampKind = TimestampKind(c.max_timestamp_kind)
supportsLanguageDetect = c.supports_language_detect
supportsTranslate = c.supports_translate
Expand Down
Loading
Loading