HypercubeCNN

September 11, 2026 · View on GitHub

Audience: contributors and anyone debugging training or performance.
Not the first document for new users — start with README.md and CPP_SDK.md.

This page describes how the core is built, not how to call it. Public API contracts live in the SDK guide; when this file and a header disagree, the header and .cpp win.

LibraryHypercubeCNNCore (C++23, CMake ≥ 3.21)
Front door (only app API)hcnn::HCNN
Orchestrator (private)hcnn::HCNNNetwork (owned by HCNN, not installed)
Version (CMake)1.0.5

Boundary policy (public vs private)

  1. Hard privateHCNNNetwork, HCNNConv, HCNNPool, HCNNReadout, and ThreadPool headers are never installed. Only HCNN.cpp and in-tree tests include them (BUILD_INTERFACE include path).
  2. Not a second SDK — do not write applications against Network/layers. Smoke may exercise them as implementation contracts.
  3. Features land on HCNN first — Network/layers gain only what the facade needs; no parallel public knobs on the private types.

HCNN is a friend of HCNNNetwork (sole public owner). Optional later: shrink Network by moving logic into HCNN.cpp without changing the public surface.


1. Module graph

  Public install:  HypercubeCNN.h / HCNN.h / HCNNTypes.h / HCNNInput.h
                   / HCNNArch.h + TrainHelpers + Spatial*


                    HCNN  (public, PIMPL)


                 HCNNNetwork          ← private (source-tree + tests only)
           ┌──────────┼──────────┐
           ▼          ▼          ▼
       HCNNConv   HCNNPool   HCNNReadout
           │          │
           └──── ThreadPool ────┘   (shared, non-reentrant)
ClassFilesOwns
HCNNHCNN.h/cppunique_ptr<HCNNNetwork>, epoch shuffle + Predict scratch
HCNNNetworkHCNNNetwork.h/cppconv/pool stacks, readout, ThreadPool, train/infer scratch
HCNNConvHCNNConv.h/cppkernel (c_out×c_in×K), bias, BN params, optimizer moments
HCNNPoolHCNNPool.h/cppno parameters (stateless reduce)
HCNNReadoutHCNNReadout.h/cppdense weights + bias + moments
ThreadPoolThreadPool.hworker threads; caller participates as thread 0

Public enums live in HCNNTypes.h: Activation, OptimizerType, PoolType, TaskType.
ReadoutGradInLoop lives on HCNNReadout (private; not on HCNN).
Default optimizer is Adam. Loss is fixed by TaskType (CE or MSE).


2. Convolution implementation (HCNNConv)

2.1 Math and layout

out[co, v] = bias[co]
           + Σ_ci  w[co,ci,SELF] * in[ci, v]              // SELF = DIM
           + Σ_ci,k w[co,ci,k]   * in[ci, v ^ (1<<k)]    // k = 0 .. DIM-1

kernel layout:  [co * c_in * K + ci * K + k]
K = DIM + 1
activations:    data[c * N + v]   (channel-major)

Self is never implemented as 1 << DIM (that shift is undefined for large DIM). It is a contiguous multiply-add over in[v].

Constraints: 3 ≤ DIM ≤ 30 for both HCNNNetwork and HCNNConv (N = 2^DIM fits in signed 32-bit int). BN backward requires the bn_save buffer from the matching forward (get_bn_save_size() = 3 * c_out: inv_std, mean, var).

2.2 Forward path structure

Per output channel:

  1. Accumulate self + neighbor weighted sums (+ bias).
  2. Optional batch norm (if enabled for the layer).
  3. Activation (NONE / RELU / LEAKY_RELU / TANH).

BN is sample-local, not mini-batch-local: mean/variance are over the N vertices of the current sample for that channel. Running mean/var (EMA, momentum 0.1, eps 1e-5) are used in eval mode. This is easy to confuse with “batch” BN over images.

Two structural variants:

  • No BN: accumulate (full-N or tiled), then activate.
  • BN: accumulate → BN (needs full-channel stats) → activate.

2.3 Two execution strategies (DIM threshold)

PathWhenPattern
Block-pair full-NNo per-layer pool or DIM < 12Contiguous half-block loads for each bit mask; auto-vectorizer friendly
Tiled XORPer-layer pool active and DIM ≥ 12Vertex tiles T = 64; in[v ^ m] style indexing over [v_begin, v_end)

Why block-pair? For mask m = 1<<k, pairs (v, v^m) sit in blocks of size 2^(k+1). Scanning low/high halves yields contiguous loads instead of gathers. That is the primary hot path for typical recipe sizes (DIM 6–11).

Why tile at large DIM? Threaded ranges may not align to block boundaries, so the threaded path keeps XOR indexing inside tiles.

Self tap is always a straight out[v] += w_self * in[v] on both paths.

2.4 Backward variants

APIRole
backward(...)Gradients + optimizer step in one call (TrainStep path)
compute_gradients(...) + apply_gradients(...)Write raw grads to caller buffers; apply after batch reduction (TrainBatch path)

Math is the same. XOR is self-inverse, so input grads reuse the same neighbor structure. Self contributes:

∂L/∂w_self = Σ_v grad_pre[v] * in[v]
∂L/∂in[v]  += w_self * grad_pre[v]

Optional post_act: for TANH, derivative is 1 - y² from cached post-activation (avoids a second tanh in backward).

2.5 TANH speed paths

  1. Post-activation derivative — as above; numerics match exact tanh derivative when y = tanh(x) is consistent with forward.
  2. HCNN_FAST_TANH (CMake, default ON) — Padé-style rational approx in forward (and derivative fallback). Private compile definition on the library; consumers inherit whatever was baked into libHypercubeCNNCore. Disable with -DHCNN_FAST_TANH=OFF.

2.6 Weight init (per conv layer)

ConditionScheme
scale > 0Uniform [-scale, scale]
ReLU / LeakyReLU and c_in > 1He/Kaiming: sqrt(6 / fan_in)
Otherwise (NONE, TANH, first layer c_in=1, …)Xavier: sqrt(6 / (fan_in + fan_out))

fan_in = c_in * K, fan_out = c_out * K, K = DIM + 1. Biases → 0; optimizer moments cleared; BN γ→1, β→0, running stats reset when present.


3. Antipodal pooling (HCNNPool)

anti_mask = (1 << DIM) - 1
for each channel, for v in [0, N/2):
  MAX: out[v] = max(in[v], in[v ^ anti_mask]); store argmax
  AVG: out[v] = 0.5 * (in[v] + in[v ^ anti_mask])
  • Output: perfect (DIM−1)-cube, N/2 vertices, same channel count.
  • No learnable parameters.
  • Not spatial 2×2 neighborhood pool — pairs maximally distant addresses.
  • Channel-parallel when DIM ≥ 14 and a thread pool is attached (disabled under batch-parallel dispatch).

4. Readout (HCNNReadout — FLATTEN linear only)

No global average pool. After the last conv/pool, activations stay channel-major [c * N + v]. The head treats that buffer as a flat feature vector:

// randomize_all_weights:
num_features = final_channels * final_N
readout = HCNNReadout(num_outputs, num_features)

// every forward / backward:
out[o] = bias[o] + sum_f W[o, f] * in[f]

Parameter count of the head:

num_outputs * (c_final * N_final) + num_outputs

Often dominates total params when pools are few and N is large (MNIST-scale recipes).

4.1 grad_in loop A/B (ReadoutGradInLoop)

Backprop into features is grad_in = W^T * grad_logits. Two loop nests (same math; pick via HCNNReadout::set_grad_in_loop only — not on the HCNN facade):

EnumNestW access
OutputOuter (default)for o: stream row into grad_insequential rows
FeatureOuterfor f: sum over ocolumn-strided

Default is OutputOuter (faster on MNIST-scale heads in Release A/B). Setting survives RandomizeWeights.


5. Training architecture (HCNNNetwork)

5.1 Shared cores

Network train entry points are thin wrappers over shared cores:

Private Network entryCoreLoss gradient
train_steptrain_step_implclassification lambda
train_batchtrain_batch_implclassification lambda per sample
train_step_regressiontrain_step_implregression lambda
train_batch_regressiontrain_batch_implregression lambda per sample

The public HCNN facade unifies these by target type (int / const int* vs const float*); Network still keeps separate entry points that share the same cores.

Shared cores own forward → loss grad → backward → weight update. Classification uses softmax CE; regression uses sum-style MSE. A new loss would be a new task path or an explicit extension of those two helpers — not a public LossType enum.

Classification CE grad (after stable softmax):
dL/d logits[i] = class_weight * (p[i] - 1[i==target]).

Regression MSE-style grad in code (sum-style, not mean):
dL/d pred[i] = pred[i] - target[i] (LR absorbs the usual 2/K mean-MSE scale).

5.2 Optimizers

Configured on all layers via HCNN::SetOptimizer (resets global Adam timestep t). Default at construction is Adam (OptimizerType::ADAM on Network and facade).

Adam with decoupled weight decay on kernels:

m = β1 m + (1-β1) g
v = β2 v + (1-β2) g²
m̂ = m / (1-β1^t),  v̂ = v / (1-β2^t)
w -= lr * (m̂ / (√v̂ + ε) + weight_decay * w)

SGD + momentum (opt-in via SetOptimizer(SGD)):

g = grad + weight_decay * w     // kernels / readout weights; not typically biases
v = momentum * v + g
w -= lr * v

t increments once per train_step / train_batch call (not per sample inside the batch).

5.3 Learning rate

Not owned by the network. Every train call takes lr explicitly. Optional helper: hcnn::cosine_lr in HCNNTrainHelpers.h.

5.4 Mini-batch parallel training

  1. LayerThreadGuard — null out per-layer thread pools (no nested ForEach).
  2. BNStatsGuard — suppress per-sample running-stat EMA races.
  3. Each sample: forward + compute_gradients into thread-local accumulators.
  4. Reduce (sum) → average → single apply_gradients pass.
  5. Recompute BN running stats from batch accumulators where needed.

Buffers: prepare_batch_buffers() lazy, then reused (allocation-free steady state). add_conv / add_pool / randomize_all_weights invalidate step, batch, and inference caches so a later prepare matches the current arch and head size. add_conv / add_pool also clear weights_initialized_; train and forward require a subsequent randomize_all_weights (head rebuild + init). set_optimizer is stored on the network and re-applied to new convs and to the rebuilt readout after randomize.

start_dim is in [3, 30] (N = 2^dim fits in signed 32-bit int). add_pool requires current_dim >= 2.

5.5 Inference

PathNotes
forwardPing-pong fwd_buf1_ / fwd_buf2_; not concurrent with another forward/train on the same instance
forward_batchPer-thread inference buffers; sample parallel

Both use EvalModeGuard: force BN eval for the call, restore prior train flags on exit (including exceptions). Safe to Forward mid-training without permanently flipping BN mode.

5.6 Epoch + shuffle (HCNN)

TrainEpoch (int labels or float targets) chunks into TrainBatch*:

shuffle_seedBehavior
0Sequential slices; zero-copy into caller buffer
nonzeroDeterministic shuffle of indices; gather into persistent scratch

Scratch grows on demand, never shrinks.

5.7 Contiguous data model

All batch/epoch APIs take one base pointer + uniform input_length (and targets). Row-major: sample i at base + i * input_length. This is intentional — pointer-per-sample APIs were rejected to avoid silent stride bugs.


6. Threading model

StrategyScopeActive when
Batch sample parallelSamples in TrainBatch / ForwardBatchpool present, batch size > 1
Vertex parallelVertices in conv F/BDIM ≥ 12, not inside batch parallel
Channel parallelChannels in pool F/BDIM ≥ 14, not inside batch parallel

ThreadPool is not reentrant: nested ForEach on the same pool deadlocks. Batch paths disable layer pools via LayerThreadGuard.

ForEach always joins workers before returning — including when the caller's own chunk throws — so functors are not left dangling on background threads.

Constructor num_threads:

ValueMeaning
0Auto (hardware_concurrency − 1 workers)
1No background workers (host may outer-parallelize nets)
N > 1N workers (+ caller as thread 0 during ForEach)

HCNNPool input dim is [2, 30] (same N-safety as conv/network).


7. Weight blob (GetWeights / SetWeights)

Requires RandomizeWeights first (WeightsInitialized()).

Included (in order), per conv:

kernel[c_out * c_in * K]
bias[c_out]                         // if bias enabled
if BN: gamma, beta, running_mean, running_var   // each c_out

then readout weights + readout bias.

Not included: SGD/Adam moments, Adam timestep.

SetWeights(blob, reset_optimizer_moments=false) restores params only (eval/export). Pass true (or call SetOptimizer) to clear moments before continuing training.


8. Embedding (network-level)

capacity = input_channels * 2^start_dim
copy raw[0 .. input_length)
zero-fill [input_length .. capacity)     // always 0, not a custom pad_value

Spatial preprocess may pad with pad_value = -1. After spatial embed, train/infer with input_length = N or the network zero-pad will wipe intentional background. Details: spatial_preprocess.md.


9. CMake / build options (core-relevant)

OptionDefaultEffect
HCNN_FAST_TANHONRational tanh in conv activate path
HCNN_NATIVE_ARCHON if top-level, OFF as subproject-march=native style host tuning (non-MSVC); packagers keep OFF
HCNN_FAST_MATHONRelaxed float flags without full associative-math chaos
HCNN_BUILD_EXAMPLESON if top-levelExamples + smoke test

When consumed via FetchContent as a subproject, examples are typically skipped.


10. File inventory

Installed public surface (apps / FetchContent / find_package):

HypercubeCNN.h           umbrella (core + optional products)
HCNN.h / HCNN.cpp        facade (PIMPL owner)
HCNNTypes.h              public enums
HCNNInput.h              full-capacity views / batches
HCNNArch.h               LayerSpec, apply_arch, HCNNConfig (header-only)
HCNNSpatialAug.h / .cpp  optional 2D aug
HCNNSpatialEmbed.h / .cpp optional 2D → length-N pack
HCNNTrainHelpers.h / .cpp optional metrics / LR / checkpoints / weight I/O

Private implementation (source tree + in-tree tests only; not installed):

HCNNNetwork.h / .cpp
HCNNConv.h / .cpp
HCNNPool.h / .cpp
HCNNReadout.h / .cpp
ThreadPool.h

In-tree only (not install surface): examples/, tests/, dataloader/.


DocUse it for
../README.mdProject hook and first path
CPP_SDK.mdPublic SDK contracts, API, optional helpers, recipes
spatial_preprocess.mdImage aug/embed contracts
Headers in HCNN*.hAuthoritative signatures and contracts