# ternary_optim — API Reference Every public symbol in `ternary_optim.h`: signature, parameters, semantics, and memory ownership. All functions are `extern "C"` and thread-agnostic (the update uses no shared mutable global state; distinct handles are independent). Row-major layout throughout: a `(rows x cols)` matrix stores element `(r, c)` at index `r * cols + c`. `numel = rows * cols`. --- ## Return codes ```c enum { TERNARY_OPTIM_OK = 0, // success TERNARY_OPTIM_EINVAL = -1, // invalid argument TERNARY_OPTIM_ENOMEM = -2, // allocation failure TERNARY_OPTIM_ENOTIMPL = -3 // not implemented (stubbed feature) }; ``` --- ## Configuration ### `ternary_optim_config` ```c typedef struct { float target_step; // avg proposed step (LSB) for a mean-|g| weight; ~0.001 int q_clamp; // symmetric accumulator clamp; 7 (15 states -> nibble) int theta; // dead-zone: w=0 while |q| 0`, `cols > 0`; `cfg` is copied, or defaults are used if NULL. - **Returns:** owning handle, or **NULL** on invalid args (`rows<=0`, `cols<=0`, `numel` overflow) or allocation failure. - **Ownership:** caller owns the returned handle and must free it with `ternary_optim_free`. The caller retains ownership of `cfg`. ### `void ternary_optim_free(ternary_optim *opt)` Releases the handle and its internal `q`. Safe with NULL. --- ## Core operations ### `void ternary_optim_materialize(const ternary_optim *opt, float *out_weights)` Writes the ternary deployed weights into a **caller-owned** buffer: ``` out[i] = 0 if |q[i]| < theta ; +1 if q[i] >= +theta ; -1 if q[i] <= -theta ``` - **Params:** `out_weights` must hold at least `numel` floats. No-op if either pointer is NULL. - **Semantics:** pure read of `q`; does not modify optimizer state. Call before every forward pass. - **Ownership:** caller owns `out_weights`. ### `void ternary_optim_step(ternary_optim *opt, const float *grad_sum, int batch_size, uint64_t batch_counter, float target_step)` One SR4 update: integrates the batch gradient into `q` (per-matrix step normalization → 8.8 fixed-point stochastic rounding with LD dither → saturating add). See [HOW_IT_WORKS.md §3–4](HOW_IT_WORKS.md). - **`grad_sum`** — per-weight gradient **summed over the batch** (not averaged), row-major, at least `numel` floats. Read-only; caller-owned. - **`batch_size`** — samples summed into `grad_sum` (`> 0`). Used for the `/batch_size` mean; may vary batch to batch (e.g. a partial final batch). - **`batch_counter`** — monotonically increasing global update index starting at 0. **Contract:** all matrices updated within the *same* logical step use the *same* `batch_counter`; the caller increments it by exactly 1 **after** stepping every matrix in that step. Drives the stateless dither. - **`target_step`** — effective per-step target (e.g. a cosine-annealed value). If `<= 0`, the config `target_step` is used. - **Side effect:** the only mutation is the integer add into `opt->q`. Nothing else persists (the dither is regenerated from the counter-hash). - **No-op** if `opt`/`grad_sum` is NULL or `batch_size <= 0`. --- ## Accessors (diagnostics) ```c int ternary_optim_numel(const ternary_optim *opt); // rows*cols, 0 if NULL int ternary_optim_rows (const ternary_optim *opt); // rows, 0 if NULL int ternary_optim_cols (const ternary_optim *opt); // cols, 0 if NULL const int8_t *ternary_optim_q (const ternary_optim *opt); // raw q view, NULL if NULL ``` `ternary_optim_q` returns a **read-only, non-owning** view of the internal `q` (one signed byte/weight, clamped to `[-q_clamp, +q_clamp]`), valid until the handle is freed. For fingerprints / rail-and-dead-zone diagnostics only; the update rule never reads it back. Do not write through it. --- ## Optional per-column output scales (decoupled helpers) These operate on **caller-owned** buffers over an arbitrary `(rows x cols)` matrix; the scale vector is **not** owned by the optimizer (it belongs to the layer). See [HOW_IT_WORKS.md §5](HOW_IT_WORKS.md) and [USAGE.md](USAGE.md). ### `void ternary_apply_col_scale(float *Z, const float *scale, int rows, int cols)` In place, `Z[r,c] *= scale[c]`. `scale` has length `cols`. Exact identity when `scale` is all-ones. No-op if `Z`/`scale` is NULL. ### `void ternary_col_scale_grad(const float *D, const float *Z_raw, float *out, int rows, int cols)` Per-column scale gradient `out[c] = Σ_r D[r,c] * Z_raw[r,c]`. `out` (length `cols`) is **overwritten**, not accumulated. `D` is the unscaled upstream delta, `Z_raw` the pre-scale/pre-bias matmul output. No-op if any pointer is NULL. Apply to the scale vector by plain SGD at a dedicated small LR. --- ## State serialization — STUBBED (TODO) Both currently return `TERNARY_OPTIM_ENOTIMPL` and perform no I/O. The thing to persist is the packed `q` (4 b/w) + shape + config; the dither/masks regenerate from the counter-hash and never need saving. Format is a follow-up. ```c int ternary_optim_save(const ternary_optim *opt, const char *path); int ternary_optim_load(const char *path, ternary_optim **out); // sets *out = NULL ```