Skip to content

Binary optimizers: inference, fit suite, and training infrastructure#1

Open
leo-gan wants to merge 9 commits into
masterfrom
feature/optimizers_new
Open

Binary optimizers: inference, fit suite, and training infrastructure#1
leo-gan wants to merge 9 commits into
masterfrom
feature/optimizers_new

Conversation

@leo-gan

@leo-gan leo-gan commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

  • Bitpacked inference & profiling: packing, XNOR+popcount linear layers, weight extract, training/inference memory profiler, scaffold inference bench and Pareto pipeline.
  • New / improved optimizers: CosineVoting, EMAFlip (recommended Bit-MLP default), SparseSign (incl. long-run max-fit recipe), HybridAccumulator, HybridV2 (paused from default suite); BN dual-Adam for binary trainers; dynamics fixes.
  • Fit-scale experiment system: fingerprint checkpoint cache under checkpoints/ (local only, gitignored), fit + re-benchmark CLIs, Bit-MLP suite plus swarm_mlp × (swarm, swarm_log, swarm_log_dynamic, adam) comparisons.
  • Docs: unified docs/optimizers.md (reasons/trade-offs, active vs paused), fit analysis, improvement proposals, SparseSign max-fit notes.

Key results (fit protocol, MNIST)

  • Bit-MLP: ema_flip best among STE FC trainers (~96.3% best test @ 15 ep); SparseSign reaches ~97.1% with 40-ep dense-hold recipe.
  • Swarm: swarm_log_dynamic and Adam-on-swarm competitive; classic recruit-rate swarm lower (~90%).
  • Checkpoints are not in git; retrain only on model/optimizer fingerprint change.

Test plan

  • uv run pytest tests/ -q (packing, binary linear, memory, optimizers, checkpoints, Pareto)
  • uv run python experiments/run_fit_training.py --default-only (cache hit or short train)
  • uv run python experiments/run_fit_training.py --swarm-only if swarm comparison needed
  • uv run python experiments/run_benchmark_checkpoints.py (eval only, no train)
  • Optional: uv run python experiments/run_full_pipeline.py (scaffold, not fit-scale)

leo-gan added 9 commits July 14, 2026 13:13
…ipeline.

Introduce XNOR+popcount binary linear layers with weight packing, training/inference memory profiling, optimizer×model training sweeps, Pareto analysis, experiments, tests, and scaffold results docs.
Scaffold empty cosine_voting, ema_flip, hybrid_accumulator, and sparse_sign optimizers; include test_debug.py for environment checks.
…timizers.

Fill previously empty modules with binary-weight optimizers (cosine-annealed voting, EMA-gated flips, adaptive integrate-and-fire hybrid, sparse sign updates) and export them from the optimizers package.
Integrate EMAFlip, CosineVoting, SparseSign, and HybridAccumulator into the training sweep, fix CosineVoting state handling, add improvement-report generation and tests, and refresh scaffold Pareto/summary docs.
Correct Hybrid fire to set weights to sign(acc), restore EMAFlip adaptive continuous updates, raise CosineVoting LR defaults, update sweep hyperparams, and add regression tests.
…izers.

Fingerprint-based save/load under checkpoints/ (gitignored); EMAFlip cosine anneal and Hybrid soft fire; SparseSign density annealing; CosineVoting restarts; fit and benchmark CLI entrypoints.
Add HybridV2 (momentum + cosine LR + sparse + soft IF), DualOptimizer for BN/bias Adam, schema fit_v3 fingerprints, and refreshed fit/benchmark reports. Checkpoints remain local-only (gitignored).
Merge optimizers.md with NEW_OPTIMIZERS into one design/trade-off guide, document fit_v3 rationale for the default, add improvement proposals, and update fit CLI/reports to drop old-vs-new framing.
… suite.

Wire swarm_mlp with swarm/swarm_log/adam into fit comparisons, improve SparseSign density hold and long-run docs, re-enable research optimizers with potential while pausing hybrid_v2, and refresh fit reports.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive benchmarking, profiling, and inference infrastructure for binary neural networks, including a bitpacked inference engine, memory profilers, and several new binary optimizers (such as ema_flip and cosine_voting). The reviewer's feedback highlights key improvement opportunities: dynamically merging optimizer states in DualOptimizer to ensure accurate memory profiling of continuous parameters, wrapping warm-up steps in a try-except block to prevent crashes on empty data loaders, and avoiding direct .data modifications in CosineVotingOptimizer by operating directly on tensors under @torch.no_grad().

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +21 to +25
# Expose state for memory profilers that inspect .state
self.state = binary_opt.state
self.param_groups = list(binary_opt.param_groups)
if bn_opt is not None:
self.param_groups = list(binary_opt.param_groups) + list(bn_opt.param_groups)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Exposing self.state = binary_opt.state in __init__ only captures the state of the binary optimizer. Since DualOptimizer also manages bn_opt (which is typically a memory-heavy optimizer like Adam), the memory profiler will completely miss the continuous parameters' optimizer state. Implementing state as a dynamic property that merges both states resolves this profiling discrepancy.

Suggested change
# Expose state for memory profilers that inspect .state
self.state = binary_opt.state
self.param_groups = list(binary_opt.param_groups)
if bn_opt is not None:
self.param_groups = list(binary_opt.param_groups) + list(bn_opt.param_groups)
self.param_groups = list(binary_opt.param_groups)
if bn_opt is not None:
self.param_groups = list(binary_opt.param_groups) + list(bn_opt.param_groups)
@property
def state(self):
combined = dict(self.binary_opt.state)
if self.bn_opt is not None:
combined.update(self.bn_opt.state)
return combined

Comment on lines +506 to +513
# Warm-up for memory profile
model.train()
xb, yb = next(iter(train_loader))
xb, yb = xb.to(device), yb.to(device)
optimizer.zero_grad()
model(xb).sum().backward()
optimizer.step()
optimizer.zero_grad()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In train_or_load, calling next(iter(train_loader)) directly can raise a StopIteration exception if the training loader is empty or has no batches. To prevent training from crashing, wrap the warm-up step in a try-except StopIteration block, similar to the implementation in training_sweep.py.

Suggested change
# Warm-up for memory profile
model.train()
xb, yb = next(iter(train_loader))
xb, yb = xb.to(device), yb.to(device)
optimizer.zero_grad()
model(xb).sum().backward()
optimizer.step()
optimizer.zero_grad()
# Warm-up for memory profile
model.train()
try:
xb, yb = next(iter(train_loader))
xb, yb = xb.to(device), yb.to(device)
optimizer.zero_grad()
model(xb).sum().backward()
optimizer.step()
optimizer.zero_grad()
except StopIteration:
pass

Comment on lines +116 to +136
if p.data.dim() < 2:
p.data.add_(p.grad.data, alpha=-bn_lr)
continue

state = self.state[p]
if "momentum_buffer" not in state:
state["momentum_buffer"] = torch.zeros_like(p.data)

buf = state["momentum_buffer"]
buf.mul_(mom).add_(p.grad.data, alpha=1.0 - mom)

if conf_thresh > 0:
vote = torch.zeros_like(buf)
confident = buf.abs() > conf_thresh
vote[confident] = torch.sign(buf[confident])
else:
vote = torch.sign(buf)
# sign(0) → 0; treat zeros as no-op already

p.data.add_(vote, alpha=-lr)
p.data.clamp_(-clip, clip)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly accessing and modifying .data (e.g., p.data.add_, p.grad.data) is discouraged in modern PyTorch as it can bypass autograd tracking, version counters, and cause silent bugs. Since the step method is already decorated with @torch.no_grad(), you can perform these operations directly on the parameter tensors.

                if p.dim() < 2:
                    p.add_(p.grad, alpha=-bn_lr)
                    continue

                state = self.state[p]
                if "momentum_buffer" not in state:
                    state["momentum_buffer"] = torch.zeros_like(p)

                buf = state["momentum_buffer"]
                buf.mul_(mom).add_(p.grad, alpha=1.0 - mom)

                if conf_thresh > 0:
                    vote = torch.zeros_like(buf)
                    confident = buf.abs() > conf_thresh
                    vote[confident] = torch.sign(buf[confident])
                else:
                    vote = torch.sign(buf)
                    # sign(0) → 0; treat zeros as no-op already

                p.add_(vote, alpha=-lr)
                p.clamp_(-clip, clip)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant