Docs Composition

Composition

KynML has three opt-in features for reuse and experimentation: import, params/$ref, and sweep. They are applied as pre-passes before semantic validation — programs that use none of them are completely unaffected.

All three are handled in kynml/compose.py (apply_composition) and kynml/sweep.py (expand_sweep). The canonical entry point that runs them is compile_to_ir in kynml/pipeline.py.


params block and $name references

A params block declares named hyperparameters with default values. Any value position in any block accepts a $name reference, which the compiler replaces with the resolved value before semantic validation.

Syntax

params:
    name = value
    ...

Values may be integers, floats, strings, booleans, or lists. The block must appear before the blocks that reference it (by convention).

$name reference positions

$name can appear anywhere a plain value is accepted:

params:
    hidden = 64
    lr     = 0.001
    bs     = 32
    epochs = 20

dataset D:
    source = csv("data/x.csv")
    target = "y"

model M:
    input 10
    dense $hidden relu    # units = 64
    dense 1 linear

train:
    model     = M
    data      = D
    loss      = mse
    optimizer = adam(lr=$lr)   # kwarg inside function call
    epochs    = $epochs
    batch     = $bs

Resolution order

  1. CLI --param key=value overrides (highest priority)
  2. params block defaults

An undefined $name (no matching key in either source) raises KynMLSemanticError:

Undefined parameter reference '$lr'. Known params: ['hidden']

CLI overrides

kynml compile model.kyn -o generated/model.py --param lr=0.01 --param hidden=128
kynml train   model.kyn --param lr=0.01

--param values are coerced: tried as int, then float, then left as str.

Python API

from kynml.parser import parse_text
from kynml.pipeline import compile_to_ir

src = """
params:
    hidden = 64
    lr     = 0.001

dataset D:
    source = csv("data/x.csv")
    target = "y"

model M:
    input 10
    dense $hidden relu
    dense 1 linear

train:
    model     = M
    data      = D
    loss      = mse
    optimizer = adam(lr=$lr)
    epochs    = 5
    batch     = 32
"""

prog = parse_text(src)

# Compile with defaults
module = compile_to_ir(prog)

# Compile with overrides (e.g. from a script or test)
module = compile_to_ir(prog, overrides={"hidden": 128, "lr": 0.005})
graph = module.graph("M")
# LinearOp.in_features=10, out_features=128
# LinearOp.in_features=128, out_features=1

import statements

Pull dataset and model blocks from another .kyn file. This lets you define shared data sources or base architectures once and reuse them across multiple training specs.

Syntax

import "relative/path/to/base.kyn"
  • One import per line. Multiple imports are allowed.
  • Must appear at the top of the file, before any block.
  • Paths are resolved relative to the importing file (not cwd).

What is imported

Only dataset and model blocks are merged. train, evaluate, export, params, and sweep blocks in the imported file are silently ignored — they are run-specific and belong to the top-level program only.

Example

shared/arch.kyn:

model BaseNet:
    input 10
    dense 64 relu
    dense 32 relu
    dense 1 linear

experiment.kyn:

import "shared/arch.kyn"

dataset D:
    source = csv("data/x.csv")
    target = "y"

train:
    model = BaseNet
    data  = D
    loss  = mse
    optimizer = adam(lr=0.001)
    epochs = 10
    batch  = 32

Error cases

Condition Error
File not found KynMLParseError: import: file not found
Circular import KynMLSemanticError: Circular import detected
Name collision with importing file KynMLSemanticError: Import conflict: dataset/model '...' is already defined
import used without source_path KynMLParseError: import statements require a source_path to resolve relative paths

Python API

resolve_imports requires source_path to compute relative paths:

from kynml.parser import parse_file
from kynml.pipeline import compile_to_ir

# compile_to_ir propagates source_path to resolve_imports automatically
module = compile_to_ir(parse_file("experiment.kyn"), source_path="experiment.kyn")

sweep block

A sweep block lists one or more parameter axes, each with a list of values. expand_sweep in kynml/sweep.py produces the Cartesian product of all combinations — one Program per combo with all $name references resolved.

Syntax

params:
    hidden = 32    # default (used when axis not swept)
    lr     = 0.001

sweep:
    lr     = [0.001, 0.01]
    hidden = [32, 64]
  • Each axis value must be a non-empty list. A bare scalar raises KynMLParseError.
  • Axes that are in sweep but not in params are valid — they act as overrides-only with no default.
  • Params not listed in sweep use their params block defaults.

Full runnable example

params:
    hidden = 32
    lr     = 0.001

sweep:
    lr     = [0.001, 0.01]
    hidden = [32, 64]

dataset D:
    source = csv("data/x.csv")
    target = "y"

model M:
    input 4
    dense $hidden relu
    dense 1 linear

train:
    model     = M
    data      = D
    loss      = mse
    optimizer = adam(lr=$lr)
    epochs    = 10
    batch     = 32

kynml sweep model.kyn expands this into 4 combinations (2 × 2) and runs them sequentially:

lr=0.001, hidden=32
lr=0.001, hidden=64
lr=0.01,  hidden=32
lr=0.01,  hidden=64

Generated per-combo scripts are written to generated/ (or --out-dir). Results are aggregated into generated/sweep_results.json, which collects the run_manifest.json from each run.

CLI

# Generate scripts + run all combos (default)
kynml sweep model.kyn

# Custom output directory
kynml sweep model.kyn --out-dir runs/

# Generate scripts without running
kynml sweep model.kyn --no-run

Python API

from kynml.parser import parse_text
from kynml.sweep import expand_sweep, generate_sweep_runner

prog = parse_text(src)
combos = expand_sweep(prog)
# combos: list of (dict, Program)
# e.g. [({'lr': 0.001, 'hidden': 32}, <Program>), ...]

for combo, resolved in combos:
    print(combo)
    # resolved is a fully-substituted Program — no ParamRef sentinels remain
    from kynml.pipeline import compile_to_ir
    module = compile_to_ir(resolved)

expand_sweep on a program with no sweep block returns a single-entry list [({}, resolved_program)] — useful for treating swept and non-swept programs uniformly.

# Generate and write a sweep orchestrator script
script_paths = [f"generated/run_{i}.py" for i in range(len(combos))]
runner_src = generate_sweep_runner(combos, script_paths, out_dir="generated/")
# runner_src is valid Python; write it to disk and execute

Interaction with Shape Inference

Composition passes run before shape inference. After substitute_params resolves all $name references, the resulting Program contains only concrete values (integers, floats, etc.). Shape inference then operates on those concrete values — so a $hidden that resolves to 64 produces exactly the same IR as if the user had written dense 64 relu directly.

Each combo in a sweep is independently compiled to IR, meaning each combo is independently shape-checked. A combo that produces a shape violation (e.g. bce with more than 1 output unit) raises KynMLShapeError for that specific combo, not for the sweep as a whole.


Formatter Support

kynml fmt (kynml/format/formatter.py) fully supports all composition constructs. The canonical output order is:

  1. import statements
  2. params block
  3. sweep block
  4. dataset blocks
  5. model blocks
  6. train block
  7. evaluate block
  8. export block

$name references round-trip unchanged through the formatter.


LSP / Diagnostics

kynml.lsp.diagnostics.diagnose(source) runs the full pipeline — parse, validate, shape inference — and never raises. Composition errors (KynMLSemanticError for undefined params, KynMLParseError for missing import files) appear as severity="error" diagnostics.

from kynml.lsp.diagnostics import diagnose

diags = diagnose(source_text)
for d in diags:
    print(f"{d['line']}:{d['col']} [{d['severity']}] {d['message']}")

See Also