Skip to content

GaussianSplatting facade training silently ignores ConfigureOptimizer settings (LR, betas, weight decay) — uses internal updates instead #1833

Description

@ooples

Problem

Handing a GaussianSplatting<float> through the facade —

await new AiModelBuilder<float, Tensor<float>, Tensor<float>>()
    .ConfigureDataLoader(loader)
    .ConfigureModel(new GaussianSplatting<float>())
    .ConfigureOptimizer(adam)  // AdamOptimizerOptions { InitialLearningRate = 1e-3, ... }
    .BuildAsync();

— appears to train (parameters do move), but the AdamOptimizerOptions the caller configured (InitialLearningRate, Beta1, Beta2, Epsilon, WeightDecay, UseAdaptiveLearningRate, ...) are silently ignored. Only MaxIterations reaches GS. Every other Adam knob is dead code as far as GS is concerned. This violates the "user can fully customize everything" design principle.

Originally filed as "training no-op" — that framing was wrong. Training happens, but via GS's own internal update path, not the facade optimizer.

Diagnostic data

Reproducer + full instrumentation lives in the discussion thread. Highlights:

Measurement Value Interpretation
gs.Layers.Count 0 GS has NO NeuralNetworkBase-tracked layers
gs.ParameterCount 0 Counts Layers.Sum(ParameterCount) = 0
gs.GetParameters().Length 472 Params exist but stored in _gaussians, outside standard Layers
Direct model.Train × 10 41 ms/step, 16/472 params moved GS's OWN training path applies updates
Facade Adam × 80 steps 21 ms/step, 16/472 params moved Same magnitude of movement — facade Adam contributes nothing
adam._lastComputedGradients after facade run length = 0 IGradientComputable.ComputeGradients returns empty vector to facade

Root cause

Since GS stores Gaussian params in _gaussians (not in Layers), the standard facade flow —

  1. AdamOptimizer.Optimize iterates epochs × batches
  2. Per batch: CalculateGradient(model, x, y) → the good path (model is IGradientComputable) calls model.ComputeGradients(x, y, LossFunction)
  3. SynthesizeTapeStepContext walks Layers.GetParameterChunks(), gets an empty context
  4. Adam's Step(context) runs but has no chunks to update

— never reaches the Gaussian parameters. What DOES update them is GS's own internal splat-gradient code running inside model.Train (called from CalculateGradient on the same code path). That internal update uses whatever LR / schedule GS's constructor / options were built with; the AdamOptimizerOptions from ConfigureOptimizer never reach it.

Same architectural pattern affects DDPMModel (Week 10 diffusion) — its params come back as a single lumped chunk (Layers.Count = n/a, GetParameterChunks = 1 chunk of 141k params). Week 10's DiffusionLab demo ships with a comment noting the facade doesn't drive DDPM's noise-prediction loop; that's the same customization gap.

Impact

  • Silent hyperparameter loss. Every downstream training run through the facade for GS is trained with whatever the GS constructor's defaults are — not what the caller configured.
  • No path to per-parameter-group LR (paper's positions=1e-4, scales=5e-3, opacity=5e-2, SH=2.5e-3). Reference implementations require this for quality; without customization it's inaccessible.
  • Same pattern lurking for other specialized models — DDPM confirmed, likely others as AiDotNet expands.

Fix direction (agreed in design discussion)

Route ConfigureOptimizer settings into the specialized model's internal updater, via an opt-in hook.

Tiny interface (say IHyperparameterAware<T>) that specialized models implement:

public interface IHyperparameterAware<T>
{
    // Called by AiModelBuilder immediately before the optimizer's first step.
    // The specialized model reads whatever knobs it needs from the passed
    // optimizer options object and stores them in its internal update state.
    void ApplyOptimizerHyperparameters(IOptimizerOptions<T> options);
}

Wiring: extend AiModelBuilder.ConfigureOptimizer (or the BuildSupervisedInternalAsync flow it feeds into) so that after the optimizer is finalized but before the first Optimize call, if model is IHyperparameterAware<T>, invoke model.ApplyOptimizerHyperparameters(optimizerOptions).

Models opting in (GaussianSplatting, DDPMModel, future specialized) each implement ApplyOptimizerHyperparameters to translate the shared options into their internal schedule (per-attribute LR groups for GS, noise-prediction-specific LR for DDPM, etc.).

Why this exceeds industry standard

  • PyTorch and reference GS implementations require users to construct parameter groups manually: torch.optim.Adam([{params: positions, lr: 1e-4}, ...]). Users must know GS's per-attribute LR conventions.
  • AiDotNet with this fix: caller passes one AdamOptimizerOptions with base LR; the model's ApplyOptimizerHyperparameters derives the per-attribute schedule automatically from the base. Industry-standard defaults live inside the model (users don't need to know GS's magic numbers), but ARE fully customizable (users can override individual attribute LRs via extended options).
  • Same pattern eliminates the customization gap for DDPM and any future specialized model — one hook, uniform behavior.

Wiring surface

ConfigureOptimizer only. AiModelBuilder.ConfigureOptimizer(...) already takes the options object; extending it to propagate settings to IHyperparameterAware models is a purely internal wiring change. No new user-facing Configure method.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions