pyjuice.optim
EM-based optimizers for training PCs. Unlike a torch.optim.Optimizer, a PC optimizer does
not operate on nn.Parameter gradients: the backward pass accumulates parameter flows into the
circuit, and the optimizer’s step consumes those flows to perform an EM update. The training loop
keeps forward/backward in your own code:
opt = juice.optim.MiniBatchEM(pc, step_size = 0.1, pseudocount = 0.01)
for x in loader:
lls = pc(x)
lls.mean().backward() # accumulates parameter flows
opt.step() # EM update, then resets the flow accumulator
- class pyjuice.optim.CircuitOptimizer(pc: TensorCircuit, pseudocount: float = 0.0, keep_zero_params: bool = False, ddp: bool = False, ddp_dtype: dtype | None = None, ddp_group=None, sync_every: int = 1)
Base class for PC parameter optimizers.
A PC optimizer drives Expectation-Maximization (EM) training of a
TensorCircuit. Unlike atorch.optim.Optimizer, it does NOT operate onnn.Parametergradients: the “gradient” of a PC is its set of parameter flows, accumulated intopc.param_flows(and each input layer’sparam_flows) by the backward pass. An optimizer’sstep()consumes those flows to perform an EM update ofpc.params.The intended loop keeps forward/backward in your own code and lets the optimizer handle the update:
opt = juice.optim.MiniBatchEM(pc, step_size = 0.1, pseudocount = 0.01) for x in loader: lls = pc(x) lls.mean().backward() # accumulates flows into pc.param_flows opt.step() # EM update, then resets the flow accumulator
step()resets the flow accumulator after each update, so you never need to callzero_flows()manually in the common case. Concrete optimizers (FullBatchEM,MiniBatchEM,Anemone) live in their own modules and define how the accumulated flows are turned into a parameter update.- Parameters:
pc (TensorCircuit) – the PC to optimize
pseudocount (float) – Laplace-smoothing pseudocount added to the parameter flows during the update
keep_zero_params (bool) – if
True, parameters that are exactly zero stay zero (no pseudocount)ddp (bool) – if
True, all-reduce the parameter flows across thetorch.distributedprocess group before every update (viaTensorCircuit.sync_param_flows()). No-op when distributed is not initialized / world size is 1.ddp_dtype (Optional[torch.dtype]) – optional reduce dtype for the DDP all-reduce (e.g.
torch.bfloat16to halve communication on bandwidth-bound interconnects); the stored flows stay float32.ddp_group – optional
torch.distributedprocess group for the all-reducesync_every (int) – DDP synchronization cadence, in EM updates.
1(default) reduces the parameter flows every update – exact synchronous DDP.> 1runssync_everylocal EM updates per rank (each on its own data shard, no flow reduction) and then averages the parameters across ranks (Local-SGD): the all-reduce happenssync_everytimes less often. Only meaningful whenddp = True. NOTE:sync_every > 1is a different optimizer than the synchronous one (averaging params after local updates is not the same as one update on averaged flows), so its convergence should be validated.
- zero_flows()
Reset the parameter-flow accumulator (
pc.param_flowsand every input layer’sparam_flows). Called automatically at the end of everystep(), so it is rarely needed explicitly.
- step(step_size: float | None = None)
Consume the accumulated parameter flows to perform one EM update, then reset the accumulator.
- Parameters:
step_size (Optional[float]) – if given, overrides the optimizer’s default step size for this step only; pass a per-step value here to reproduce a learning-rate schedule without a scheduler.
- class pyjuice.optim.FullBatchEM(pc: TensorCircuit, pseudocount: float = 0.0, keep_zero_params: bool = False, ddp: bool = False, ddp_dtype: dtype | None = None, ddp_group=None, sync_every: int = 1)
Full-batch EM. Accumulate parameter flows over the entire dataset, then perform a single exact EM M-step (
step_size = 1.0). Use onestep()per epoch:opt = juice.optim.FullBatchEM(pc, pseudocount = 0.01) for epoch in range(num_epochs): for x in loader: lls = pc(x) lls.mean().backward() # flows accumulate over the whole epoch opt.step() # one exact EM update, then reset
See
CircuitOptimizerfor the constructor arguments.- step(step_size: float | None = None)
Consume the accumulated parameter flows to perform one EM update, then reset the accumulator.
- Parameters:
step_size (Optional[float]) – if given, overrides the optimizer’s default step size for this step only; pass a per-step value here to reproduce a learning-rate schedule without a scheduler.
- class pyjuice.optim.MiniBatchEM(pc: TensorCircuit, step_size: float = 0.1, niters_per_update: int = 1, pseudocount: float = 0.0, keep_zero_params: bool = False, ddp: bool = False, ddp_dtype: dtype | None = None, ddp_group=None, sync_every: int = 1)
Mini-batch EM. Perform an EM update every
niters_per_updateminibatches, blending the old and newly-estimated parameters bystep_size:opt = juice.optim.MiniBatchEM(pc, step_size = 0.1, pseudocount = 0.01) for x in loader: lls = pc(x) lls.mean().backward() opt.step()
- Parameters:
The remaining arguments are those of
CircuitOptimizer.- step(step_size: float | None = None)
Consume the accumulated parameter flows to perform one EM update, then reset the accumulator.
- Parameters:
step_size (Optional[float]) – if given, overrides the optimizer’s default step size for this step only; pass a per-step value here to reproduce a learning-rate schedule without a scheduler.
- class pyjuice.optim.Anemone(pc: TensorCircuit, step_size: float = 0.4, momentum: float = 0.9, niters_per_update: int = 1, pseudocount: float = 1e-06, keep_zero_params: bool = False, ddp: bool = False, ddp_dtype: dtype | None = None, ddp_group=None, sync_every: int = 1)
The Anemone optimizer: scaled mini-batch EM with momentum.
It accumulates parameter flows over
niters_per_updateminibatches and then performs a flow-rescaled EM update (mini_batch_em(..., step_size_rescaling = True), i.e. the “mini_em_scaled” objective that normalizes by the accumulated flow mass). Whenmomentum > 0, the accumulated flows are first passed through a bias-corrected exponential moving average (the same scheme as Adam-style momentum) before the update:opt = juice.optim.Anemone(pc, step_size = 0.4, momentum = 0.9, niters_per_update = 8, ddp = True) for x in loader: lls = pc(x) lls.mean().backward() opt.step() # fires the update every `niters_per_update` minibatches
The momentum is applied to both
pc.param_flowsand each input layer’sparam_flowsas:f <- (1 - momentum) * f buffer <- momentum * buffer + f f <- buffer / (1 - momentum ** (update_count + 1)) # bias correction
- Parameters:
The remaining arguments are those of
CircuitOptimizer(pseudocountdefaults to1e-6here, matching typical Anemone training).- step(step_size: float | None = None)
Consume the accumulated parameter flows to perform one EM update, then reset the accumulator.
- Parameters:
step_size (Optional[float]) – if given, overrides the optimizer’s default step size for this step only; pass a per-step value here to reproduce a learning-rate schedule without a scheduler.