6.1 KiB
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
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
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|<theta, else sign(q); 2
int use_dither; // 1 = stratified Weyl LD dither (default); 0 = i.i.d.
uint64_t seed; // threaded through every weight/every update + init
int64_t idx_base; // global index base for this matrix in the hash lane
} ternary_optim_config;
The struct is copied into the handle at create time; changing it afterward has
no effect. q_clamp and theta are floored to 1 internally if set below 1.
void ternary_optim_config_default(ternary_optim_config *cfg)
Fills cfg with the reference defaults: target_step=0.001, q_clamp=7,
theta=2, use_dither=1, seed=42, idx_base=0. No-op if cfg is NULL.
Ownership: caller owns cfg.
Lifecycle
ternary_optim *ternary_optim_create(int rows, int cols, const ternary_optim_config *cfg)
Allocates a handle owning the numel-byte int4 accumulator q, initialized to
{-3, 0, +3} at {33.5%, 33%, 33.5%} (drawn from an internal xorshift seeded
from seed ^ idx_base).
- Params:
rows > 0,cols > 0;cfgis copied, or defaults are used if NULL. - Returns: owning handle, or NULL on invalid args (
rows<=0,cols<=0,numeloverflow) or allocation failure. - Ownership: caller owns the returned handle and must free it with
ternary_optim_free. The caller retains ownership ofcfg.
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_weightsmust hold at leastnumelfloats. 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.
grad_sum— per-weight gradient summed over the batch (not averaged), row-major, at leastnumelfloats. Read-only; caller-owned.batch_size— samples summed intograd_sum(> 0). Used for the/batch_sizemean; 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 samebatch_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 configtarget_stepis 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_sumis NULL orbatch_size <= 0.
Accessors (diagnostics)
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 and 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.
int ternary_optim_save(const ternary_optim *opt, const char *path);
int ternary_optim_load(const char *path, ternary_optim **out); // sets *out = NULL