Foundry/ternary_optim/docs/API.md

6.1 KiB
Raw Permalink Blame History

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; 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.

  • 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)

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