Export and conversion¶
kanforge.convert
¶
Move models between PyTorch, MLX and JAX, and save / load them anywhere.
A model is described by a JSON-able spec (architecture, basis configs,
constructor options, activation names) plus a flat dict of NumPy arrays
named like torch's state_dict. Supported: KANLinear, GRKAN,
KAN, MultKANLinear, MultKAN and sequences of those with
Linear layers and standard activations (models from from_mlp).
mlx_model = convert(torch_model, "mlx")
save(model, "model.npz"); model = load("model.npz", backend="jax")
activation_name(act)
¶
Portable name of a base activation / activation layer (None = absent).
make_activation(name, backend)
¶
Callable for name on backend ("torch", "mlx", "jax").
to_spec(model)
¶
(spec, arrays) of a model of any backend.
from_spec(spec, arrays, backend)
¶
Build the model described by spec on backend and load arrays.
convert(model, backend)
¶
The same model (architecture and weights) on another backend.
save(model, path)
¶
Backend-independent file: arrays and the JSON spec in one .npz.
load(path, backend='torch')
¶
Load a file written by save on any backend.
kanforge.export.lut
¶
Lookup-table form of KAN models: the deployable representation.
Every edge phi_oi of a layer is tabulated at size uniform points over
its input range; a layer then computes
t = (x_i - lo_i) / step_i, j = clip(floor(t), 0, size - 2), f = t - j
y_o = b_o + sum_i T[i, j, o] + f * (T[i, j+1, o] - T[i, j, o])
i.e. gather + lerp + sum — operations every runtime (ONNX, Core ML, FPGA
logic) has. Outside the range every edge continues linearly with its end
slope (extrapolate="linear", which follows the SiLU base branch; the
slopes are float and estimated over the last 1/32 of the table, so
quantisation noise does not leak into them) or is held ("clamp"). A
table is only as good as its range: choose margin / samples so that
inputs stay inside. Works for models of any backend; MultKAN product nodes,
Linear layers and standard activations are kept as such.
quantize(lut, bits) stores the tables as signed integers with one scale
per output and a per-edge offset folded into the bias, so a layer sums
integers and multiplies once per output.
LUTLayer(tables, lo, hi, bias, n_sum=None, n_mult=0, arity=2, scale=None, bits=None, slope_lo=None, slope_hi=None)
dataclass
¶
Tables (in, size, out) over per-input ranges [lo, hi].
end_slopes(tables)
¶
(slope_lo, slope_hi) per index step, each (in, out), from float tables.
to_lut(model, x=None, size=256, margin=0.05, extrapolate='linear')
¶
Tabulate a model (any backend). Ranges come from the activations every
layer receives for samples x (widened by margin), or from the grids.
quantize(lut, bits=8)
¶
Integer tables: T[i, :, o] = c_io + s_o * q[i, :, o], |q| <= 2^(bits-1) - 1.
kanforge.export.onnx_export
¶
ONNX export.
export_onnx(model_or_lut, path, x)— builds the graph directly from the lookup-table form (gather + lerp + sum per layer, int8 tables with a scale per output, product nodes, Linear layers, activations). Independent of the training backend: PyTorch, MLX and JAX models export the same way. Runs in onnxruntime and any ONNX runtime with opset 17.export_onnx_exact(torch_model, path, example)— the exact network (basis functions as arithmetic) throughtorch.onnx.export.
onnx_model(lut)
¶
ONNX ModelProto of a lookup-table model; input "x" (batch, n_in) float32.
export_onnx(model, path, x=None, size=256, bits=None, margin=0.05, extrapolate='linear')
¶
Write model (any backend, or a LUTModel) as an ONNX lookup-table network.
export_onnx_exact(model, path, example, opset=OPSET)
¶
The exact PyTorch network as ONNX (torch.onnx.export, TorchScript exporter).
kanforge.export.coreml
¶
Core ML export (coremltools): Apple devices, Neural Engine / GPU / CPU.
export_coreml(model_or_lut, path, x)— the lookup-table network as an ML Program built op by op (gather + lerp + sum; int8 tables stored as int8 withconstexpr_affine_dequantize), for models of any backend. Needs macOS 13 / iOS 16 or newer.export_coreml_exact(torch_model, path, example)— the exact network throughtorch.jit.traceandcoremltools.convert.
coreml_model(lut, max_batch=65536)
¶
coremltools.models.MLModel of a lookup-table model (input "x", output "y").
export_coreml(model, path, x=None, size=256, bits=None, margin=0.05, extrapolate='linear')
¶
Write model (any backend, or a LUTModel) as a Core ML lookup-table network.
export_coreml_exact(model, path, example, max_batch=65536)
¶
The exact PyTorch network as Core ML (traced).
kanforge.export.fpga
¶
FPGA generator: KAN layers as integer lookup tables in Verilog (KANELÉ-style).
Every edge becomes a ROM indexed by its quantised input, so a layer is memory + adders, no multipliers in the datapath:
- inputs of a layer are
A-bit addresses: a_i = clamp(round((x_i - lo_i) / step_i)) - edge (i, o) stores q[i, a, o] = round((phi_oi(lo_i + a step_i) - c_io) / s_o),
Wbits - acc_o = sum_i q[i, a_i, o] (adder per output)
- real output y_o = s_o * acc_o + b_o (b_o includes sum_i c_io)
- next layer's address: a'_o = clamp((acc_o * M_o + K_o) >>> SH, 0, 2^A - 1) with integers M_o, K_o chosen so that a'_o = round((y_o - lo'_o) / step'_o)
One register stage per layer; the last layer outputs the accumulators and
design.json holds s_o and b_o to turn them into real values.
simulate(design, x) is the bit-exact NumPy model of the generated logic;
the testbench compares the RTL against vectors from it.
design(model, x=None, address_bits=8, value_bits=8, margin=0.05)
¶
Integer design of a KAN (any backend, or a float LUTModel with 2^A points).
quantize_input(d, x)
¶
A-bit addresses of real inputs (what the host feeds the FPGA).
simulate(d, x=None, addresses=None)
¶
Bit-exact model of the RTL: (last-layer accumulators, real outputs).
write_verilog(d, out_dir, test_inputs=None)
¶
Write RTL, ROM images, a self-checking testbench and design.json.
export_fpga(model, out_dir, x=None, address_bits=8, value_bits=8, margin=0.05, test_vectors=64)
¶
design + write_verilog with a testbench on test_vectors samples of x.
kanforge.grid
¶
Grid tools, the same for every basis and backend (NumPy float64).
adaptive_grid— pykan's data-driven grid: quantiles of the inputs mixed with a uniform grid over their range (grid_epsof uniform).refit— move a layer to a new basis or grid without changing what it computes: tabulate the old edge functions on samples and solve a least squares problem per input in the new basis. Grid update, grid extension (refinement) and conversion between bases (e.g. B-spline -> Chebyshev) are all this one operation.
adaptive_grid(x, n_points, margin=0.0, grid_eps=0.02)
¶
Per-input grids (in, n_points) from samples x of shape (N, in).
resample_grid(grid, n_points)
¶
Interpolate grids (in, n) to n_points points, keeping their shape.
grid_ranges(basis, params)
¶
Per-input (lo, hi) of the basis grid, (in, 2), or None without a grid.
sample_inputs(ranges, n)
¶
n uniform points per input inside ranges (in, 2): (n, in).
refit(old_basis, old_params, old_coef, new_basis, new_params, x, ridge=0.0)
¶
Coefficients (out, in, new_basis.size) reproducing the old edge functions.
x (N, in) are the points where they must match; params are
expanded per input. Solved per input with lstsq (minimum-norm when a
basis function has no samples), so a new basis that contains the old one
reproduces it to rounding error. ridge > 0 adds Tikhonov damping,
relative to the mean squared basis value.
kanforge.regularization
¶
Sparsity regularisation of KAN layers, written once for every backend.
pykan's penalty works on edge magnitudes |phi_oi| averaged over a batch: L1 of the magnitudes plus the entropy of their row and column distributions, which pushes a layer towards few important edges (needed before pruning and symbolic regression). Optional terms penalise the coefficients themselves and their differences (smoothness). Without data, efficient-kan's variant uses mean |coef| as edge magnitude.
kanforge.flops
¶
FLOPs and parameters of KAN and MLP models, for comparisons at equal cost.
Counts are per sample, forward pass, following KANbeFair's convention: a
multiply-add is 2 FLOPs, elementwise arithmetic 1, a transcendental
function (exp, tanh, sin, ...) NONLINEAR FLOPs (the same constant for
KANs and MLPs, so neither side is favoured). Basis evaluation is counted
from the algorithm the plain-ops path runs.
kanforge.kernels.plan
¶
What a fused kernel has to compute for a basis, independent of the backend.
A KernelPlan describes the per-input kernel features phi_j(x) that the
generated code evaluates in registers, and how derivatives are obtained:
chebandfourierare folded: the m-th x-derivative of every basis function is a fixed linear combination of the kernel features (phi_k^(m) = sum_j R_m[k, j] T_j), so kernels never differentiate andR_mis applied to the coefficients outside the kernel. All polynomial families with a tanh or identity input map reduce to Chebyshev features.bsplineandbumpare native: the derivative order is compiled into the kernel.