ds4-winhip / docs /rust-migration /FFI_CONTRACT.md
staccs's picture
ds4-winhip: Windows/HIP 7.2 build tree, launcher, supervisor, bench + notes (part 6)
5396c20 verified
|
Raw History Blame Contribute Delete
14.9 kB

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

  1. Do not bindgen ds4.h, ds4_gpu.h, or cuda/mmq/*.h.
  2. Keep native engine/device structures opaque. Only the documented host-owned metadata descriptors and scalar/POD options cross by layout.
  3. Do not put CUstream, device pointers, MMQ descriptors, or graph execs in Rust application code.
  4. unsafe belongs in ds4-sys or a tiny reviewed native/OS adapter. Application policy/runtime code showing unsafe { is an architecture defect; localized POSIX/linenoise adapters must carry a SAFETY rationale.
  5. Errors cross the boundary as int + caller-provided char *err buffer, matching the existing C session helpers. No C++ exceptions. No Rust panic across FFI.
  6. 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).
  7. Every successful *_create / *_open has exactly one *_free. Rust Drop is 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/stats text is host-owned; live census + observation copy through ds4_bridge_mem_*_snap, not a bindgen of ds4_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.