Download docs/rust-migration/FFI_CONTRACT.md from staccs/ds4-winhip: direct link, hf CLI and curl.
- Browser
- Download file 14.9 kB
-
https://huggingface.co/staccs/ds4-winhip/resolve/main/docs/rust-migration/FFI_CONTRACT.md
- Command line
-
hf download hf://staccs/ds4-winhip/docs/rust-migration/FFI_CONTRACT.md
-
curl -L -o FFI_CONTRACT.md https://huggingface.co/staccs/ds4-winhip/resolve/main/docs/rust-migration/FFI_CONTRACT.md
FFI contract
Rust talks to the native inference runtime through one header:
native/bridge/ds4_bridge.h.
NVTX is host observability: ds4-cli uses the optional official NVIDIA Rust
SDK directly. Profiler symbols and handles do not belong in this ABI,
ds4-sys, or ds4-core. The optional ds4-perf-gpu helper isolates
profiling-only CUDA/CUPTI calls and callbacks behind maintained cudarc bindings.
That adapter is independent of this inference ABI; see the
profiling contract.
Rust application
↓
ds4-core (safe)
↓
ds4-sys (unsafe)
↓
ds4_bridge.h ← the source-matched inference ABI
↓
ds4_bridge.c
↓
existing ds4.h / ds4.c / ds4_cuda.cu internals
Hard rules
- Do not bindgen
ds4.h,ds4_gpu.h, orcuda/mmq/*.h. - Keep native engine/device structures opaque. Only the documented host-owned metadata descriptors and scalar/POD options cross by layout.
- Do not put
CUstream, device pointers, MMQ descriptors, or graph execs in Rust application code. unsafebelongs inds4-sysor a tiny reviewed native/OS adapter. Application policy/runtime code showingunsafe {is an architecture defect; localized POSIX/linenoise adapters must carry aSAFETYrationale.- Errors cross the boundary as
int+ caller-providedchar *errbuffer, matching the existing C session helpers. No C++ exceptions. No Rust panic across FFI. - Call-scoped strings and token buffers are borrowed until return. The bridge must copy retained data or document a longer borrow backed by a live Rust owner (for example, model vocabulary strings).
- Every successful
*_create/*_openhas exactly one*_free. RustDropis the only application-side destructor.
Opaque handles and safe ownership
ds4_bridge_model, ds4_bridge_session, snapshots and batch contexts are
opaque handles. The safe wrappers own them
through NonNull and matching Drop implementations. Session<'m> borrows
its Model; snapshots and batches preserve the same model lifetime. Native
execution handles are thread-affine, not transferable application state.
Do not typedef these handles to ds4_engine / ds4_session. The bridge
structs may contain those pointers; Rust callers must not inspect them.
Selected ABI operations
The header is the authoritative symbol list. This table explains contracts used by the host and retained C parity helpers; it is not an export-count freeze or permission to bind native internals.
| Function | Meaning |
|---|---|
ds4_bridge_bind_plan_check |
host inventory + required-name table; 0 if native can consume it |
ds4_bridge_bind_plan_match |
host vs native slot identity (name/need/found/type/dims/offsets/bytes/shard + remap) |
ds4_bridge_model_open |
mmap-backed ds4_engine_open; optional opt->plan is checked first; optional opt->tensors replaces parse_tensors (owned name copies); optional opt->shape applies the pinned C literal + DeepSeek compress table and skips config_validate_model; optional opt->vocab applies host token/merge/specials and skips vocab_load; optional opt->bind is the host name→tensor-dir index so model_find_tensor skips the C name walk and the main-model weights_validate_layout |
ds4_bridge_model_boot_prewarm |
idempotent ds4_engine_boot_prewarm; server calls it after batch placement and before listen/accept |
ds4_bridge_model_free |
ds4_engine_close |
ds4_bridge_drafter_shared |
read-only DSpark IPC import result; zero for NULL, absent or failed imports |
ds4_bridge_session_create |
ds4_session_create |
ds4_bridge_session_free |
ds4_session_free |
ds4_bridge_session_sync |
prefix tokens → ds4_session_sync |
ds4_bridge_session_sync_cb |
one ds4_session_sync plus call-scoped durable prefill_chunk frontiers |
ds4_bridge_sync_step37 |
validated Step image crops and expanded prompt → synchronous native vision encoding and full target/MTP refill |
ds4_bridge_eval |
ds4_session_eval one token |
ds4_bridge_iquest_trial |
recursive embedded MTP proposals and sequential target verification; caller-owned token/argmax arrays; returns pending trial width, 0 for ordinary fallback, or -1 on failure |
ds4_bridge_iquest_commit |
commit an accepted prefix of the pending trial; restore rejected target/MTP ring writes and normalized hidden carry before publishing the frontier |
ds4_bridge_glm53_trial |
embedded proposals and width-one target verification; borrowed token/argmax outputs, pending native journal, 0 for ordinary fallback and -1 on failure |
ds4_bridge_glm53_commit |
restore the recorded accepted prefix, including recurrent state, pool tails and predictor frontier; never re-forward to commit |
ds4_bridge_session_argmax |
greedy next id |
ds4_bridge_session_pos |
native committed timeline (host SessionLedger is authoritative) |
ds4_bridge_session_ctx |
session context length |
ds4_bridge_session_rewind |
ds4_session_rewind |
ds4_bridge_session_invalidate |
ds4_session_invalidate |
ds4_bridge_session_generation |
Inc 5a content generation |
ds4_bridge_session_prefill_cap |
chunk cap used by the native graph |
ds4_bridge_session_exaone_rewind_span |
EXAONE sliding-window reuse span |
ds4_bridge_session_sample |
ds4_session_sample (caller-owned rng) |
ds4_bridge_session_save_payload |
path wrapper over ds4_session_save_payload (native writes header+tokens+GPU tail) |
ds4_bridge_session_load_payload |
path wrapper; payload_bytes = file size |
ds4_bridge_session_load_payload_range |
path + checked offset/length; native consumes exactly that embedded DSV4 range |
ds4_bridge_tokenize_text |
caller-owned int32_t * + cap + n_out |
ds4_bridge_tokenize_rendered_chat |
same buffer contract, special-token path |
ds4_bridge_token_text |
caller-owned byte buffer; C frees the malloc |
ds4_bridge_token_eos |
engine EOS / family EOT |
ds4_bridge_token_is_stop |
ds4_token_is_stop (1/0) |
ds4_bridge_model_id |
ds4_engine_model_id (syntax dispatch) |
ds4_bridge_spec_snapshot |
cumulative process-global draft/hit/quench counters; individually atomic reads into caller-owned POD |
ds4_bridge_mem_census_snap |
process-global CUDA census image (seqlock + last-stable torn cache); supported=0 when the backend keeps no census |
ds4_bridge_mem_observe_snap |
typed observation (status/source + free/total/cuda_free/meminfo) |
ds4_bridge_mem_substrate_outstanding |
ds4_gpu_substrate_outstanding (0 on Metal/CPU stubs) |
ds4_bridge_session_sync_cb borrows its token buffer, callback, and userdata
until the call returns. The callback runs synchronously on the calling thread,
receives only native prefill_chunk events (never prefill_display), and is
cleared before both successful and failed returns. It must not re-enter
sync/eval. The safe wrapper exposes a scoped PrefillCheckpoint instead of the
session; that value permits exact prompt[..current] inspection and synchronous
payload save only. Rust contains callback panics before they reach C, and the
host ledger commits the full prompt only after native sync succeeds.
Family/shape identify (ds4-core::identify_gguf) is host-owned: it
mmaps GGUF metadata and does not call ds4_bridge_model_open.
Tensor inventory + -0000N-of- shard remap (ds4-core::TensorInventory)
is also host-owned; Model::open builds that plan before the bridge
call. The family weights_bind name catalog (ds4-core::BindPlan) is
host-owned and is passed as ds4_bridge_bind_plan so native
ds4_bridge_bind_plan_check consumes it before ds4_engine_open.
The full host inventory (ds4_host_tensor_dir / opt->tensors) is
installed for that open. When present, native skips parse_tensors
and applies the host table (names are strdup'd; the Rust
CStrings die when Model::open returns). Optional opt->shape
(ds4_host_shape) is the host config_validate result: native
applies the pinned g_ds4_shape literal plus the verified DeepSeek
compress table and skips C config_validate_model. Optional opt->bind (ds4_host_bind_map) is the host-resolved
name→tensor-dir index: when installed, model_find_tensor uses it
instead of scanning m->tensors. Names not in the map (MTP/DSpark
siblings) fall back to the C walk. When that map is installed, native
also skips the main-model weights_validate_layout because
Model::open already ran the host table. Host owns the DeepSeek
MTP/DSpark sibling name and expected-layout catalogs (mtp-flash /
dspark-pro) and can resolve/validate a sibling BindPlan against a
host inventory (--bind-names mtp-flash --bind-plan). Optional
opt->mtp_path / opt->dspark_path attach the DeepSeek-only sibling
support models through the same open; the optional opt->mtp_bind /
opt->dspark_bind maps are host-resolved name→index tables for THAT
sibling's tensor dir (Model::open_with_support runs sibling
resolve + expected-layout validation first). Native keeps them in
separate slots, swaps each into the active map only around its own
sibling open+bind window, and skips that sibling's C layout check —
sibling pointer assignment stays C. After base
weights_bind, native clears host tensor-dir / bind-map / vocab /
shape so a later sibling model_open cannot apply the base GGUF
tables. The C CLI/server
leave tensors/shape/vocab/bind and the sibling paths/maps NULL, so
the GGUF cursor walk, C validate,
C vocab_load, C model_find_tensor name walk, and C
weights_validate_layout (base and sibling) stay the C oracle
behavior. Weight upload / VMM bind stay native. Tokenizer encode / decode / special / stop
(ds4-core::Vocab) is host-owned; Model keeps the Vocab so
native token-string pointers stay valid for the engine lifetime.
--tokenize / --validate still must not open the engine. Session timeline / sync plan /
rewrite / rewind / generation (ds4-core::SessionLedger) is host-owned;
Session::pos / generation read the ledger. The DSV4 payload prefix
(13×u32 LE header + token ids; magic DSV4, version 3) is host-owned
(ds4-core::payload). Session::load_payload_range reads only that
prefix, rejects overflow / out-of-file ranges before FFI, and passes the
bounded range to native. A bridge-side preflight failure preserves the
existing host ledger; failures after native load begins follow the native
generation and invalidate the host checkpoint. GPU / logits / family tensor
tails stay native.
The Inc 5 continuation registry (ds4-server::ContRegistry) is
host-owned: publish / resolve / hold / pin / TTL / bank claim do not
cross FFI. Weight bind and native prefill/eval still go through the
bridge. Model tokenize/stop/eos/token_text use the host Vocab.
The bridge tokenize helpers remain for the C engine path.
Open options stay a small C struct of scalars, const char *
paths, and optional borrowed plan/tensor-dir/shape/vocab/bind pointers
(model path, backend enum, thread count, defer-prewarm, then the
optional mtp_path / dspark_path / mtp_bind / dspark_bind
sibling fields appended at the end). Do
not pass ds4_engine_options by value into Rust — that struct will
keep growing on the C side and is not the ABI.
SSD enable/cold flags and expert-count/byte budgets cross as scalar options; file I/O, cache slots and device handles remain native. The POD layouts are source-matched: rebuild Rust and the bridge together after an options change.
ds4_host_tensor_dir can carry a named-part mapping plan (n_shards,
shards). Zero retains the legacy single-file/numbered-split loader.
Rust validates the manifest and supplies borrowed path/size/base descriptors;
native rechecks sizes, page placement and tensor bounds before mapping each
file into the reserved range. It retains file descriptors, not the borrowed
paths. No tensor payload or device handle crosses this boundary.
Token arrays are const int32_t * + length. Do not export
ds4_tokens.
Ownership
| Object | Allocated by | Freed by | Rust view |
|---|---|---|---|
ds4_bridge_model |
bridge | ds4_bridge_model_free |
NonNull, Drop |
ds4_bridge_session |
bridge | ds4_bridge_session_free |
NonNull, Drop |
| error buffer | Rust caller | Rust caller | &mut [u8] scratch |
| token scratch | Rust caller | Rust caller | &[i32] |
| Step normalized CHW crops | Rust caller | Rust caller after synchronous sync | borrowed F32 buffers and checked span descriptors |
| GPU tensors / graphs | native session | native session free | invisible |
The bridge must not return interior pointers into engine arenas.
Step image sync retains only native-owned GPU features. Rust preflights the complete image/token budget before decoding; native code rechecks dimensions, finite pixels and placeholder coverage before GPU mutation. Rejected preflight preserves the frontier. A failure after encoding begins invalidates target/MTP state and advances the session generation so Rust discards its checkpoint. Changed images force refill even when the expanded prompt tokens are identical.
Native boundary scope
These remain C-internal or later narrow additions with their own review, not dump-the-header expansions:
ds4_gpu_*(thousands of lines of tensor ops)ds4_batch_ctx/ reclaim / governor ledgers (the/metrics/v1/statstext is host-owned; live census + observation copy throughds4_bridge_mem_*_snap, not a bindgen ofds4_mem_census.h)- family-specific test hooks (
ds4_engine_motif3_*, dots3 logits) - Metal / CUDA types
- distributed pthread/socket internals
- KV store
kv_buf/ malloc arena
KV and distributed protocols use explicit codecs and file-format modules in Rust. They do not ride on a giant bindgen. New native exports require a concrete inference need, reviewed ownership and relevant parity evidence; keep host policy and profiling annotations out of this boundary.
Linkage
Native Rust builds link the selected Make backend objects plus
native/bridge/ds4_bridge.c. The CUDA Driver API, CUDA Runtime, and
cuBLAS stay on the native link line (-lcuda -lcudart -lcublas).
Rust must not add a second CUDA stack (cudarc-driven kernels, a second context, a second VMM arena).
Versioning
The bridge is a source-matched contract. A signature or descriptor change
updates the header, native implementation, ds4-sys, safe wrappers and
relevant parity tests together. The production Rust consumer is ds4-sys;
C proof programs may also include the bridge header.
There is no promise of binary compatibility with out-of-tree callers. The v0.1.0 boundary establishes stable ownership and behavior; it does not freeze an obsolete migration-era symbol count.