If you trained an OxiGAF avatar on 0.1.1, the optimizer was not doing what LossConfig told it to do — and if sh_degree >= 1, it was doing that wrong thing with half a gradient missing.
Today we released OxiGAF 0.1.2 — a correctness release built around two GPU backward-pass bugs that affected every 0.1.1 training run, unconditionally, plus pure-Rust PyTorch/.pkl checkpoint ingest, spec-conformant glTF 2.0 export, and the first real meta-learning avatar model.
No C. No C++. No Fortran. OxiGAF reconstructs an animatable 3D Gaussian avatar from a single casual video and compiles to a single static binary — the GPU paths run through wgpu on Metal, Vulkan, and DirectX alike. 0.1.2 tightens that sovereignty further: switching the candle dependency to the COOLJAPAN fork oxicandle-core/oxicandle-nn drops C Oniguruma (onig/onig_sys) out of the tokenizer path, and oxigaf-cli’s HTTP stack drops OpenSSL and then ring entirely — hf-hub is gone, replaced by a direct ureq + rustls client (RustCrypto via oxitls-rustcrypto-provider) with native, byte-accurate download progress instead of shelling out to curl.
Why 0.1.2 is a game changer
0.1.1 shipped a Gaussian-Splatting trainer that looked correct and wasn’t:
Trainer::compute_gradientshardcoded an L2 photometric loss.LossConfig’sw_l1,w_ssim, andw_ms_ssimweights changed what got logged — never the direction the optimizer actually descended.- Every
sh_degree >= 1model trained with an incomplete position gradient. The forward pass evaluates view-dependent spherical-harmonics color as a function of Gaussian position; the backward shader (preprocess_bwd.wgsl) differentiated the projection path but not the color path. - A WGSL workgroup-uniformity bug could attribute a tile’s gradient to the wrong Gaussian. The backward tile kernel’s reverse-traversal loop bound was read per-pixel around a
workgroupBarrier()call — non-uniform control flow WGSL requires to be uniform — so thread 0 could flush a 256-thread gradient sum onto whatever Gaussian it happened to be visiting. - Position/scale/opacity regularization losses were computed for logging only.
Gradients::offsetwas never written by anything, sow_position_regcould never actually move a Gaussian. - The finite-difference verification harness itself had a bug:
gpu_gradient_verify.rsreported a “clean”0.0error for a NaN,Infinity, or empty comparison — which is part of why none of the above was caught earlier.
0.1.2 ends all of that:
- The photometric gradient now matches the configured loss. A new
image_gradientmodule implements the real L1/SSIM/MS-SSIM pixel adjoint, built from the installedLossComputer’s actual config. preprocess_bwd.wgsladds the missing∂L/∂color → ∂L/∂dir → ∂L/∂poschain for everysh_degree >= 1model.rasterize_bwd.wgslcomputes one workgroup-uniform loop bound (tile_end, via tree reduction +workgroupUniformLoad) instead of a per-pixel one, with per-thread validity now acontributesmask.- Regularization and score-distillation (SDS) gradients are wired in for real —
add_regularization_gradientswrites directly intoGradients::offset/scale/opacity, andDiffusionTargetGenerator::compute_sds_gradientgives SDS training an actual backward path for the first time. - The verification harness’s own NaN/Infinity/empty-result bugs are fixed, covered by new regression tests so a regression this severe can’t hide behind a false “clean” pass again.
All under the same discipline as every OxiGAF release: 15,289 / 15,289 tests passing (28 skipped, GPU/slow-only — separately confirmed passing on real Metal hardware), zero unwrap(), every file under 2,000 lines.
Technical Deep Dive: where the fixes landed
-
oxigaf-render— the backward shaders got a correctness pass. Beyond the four gradient fixes above (including a cull guard againstinf * 0 = NaNfor Gaussians at/behind the near plane, and a rewritten NaN-proof skip test), the crate also gains a real (non-approximating) Lottes tone-mapping operator, upfront device-limit validation (rasterizer_device_limits) so an under-provisioned GPU fails fast instead of hitting an opaque pipeline-validation panic later, a GPU-sideGpuTimestampProfiler, and a single spec-conformantgltfmodule consolidating what were three independently-written, mutually-incompatible glTF emitters in the workspace. -
oxigaf-trainer— the gradient path is real end to end. Beyond the fixes,LrScheduleConfig(6 variants: Fixed, WarmupCosine, Cosine, Step, Exponential, Cyclic) andGradientClipConfigmake schedules and clipping declarative instead of hand-wired;pruning::GaussianPruneraddsprune_by_min_scale/prune_to_sparsity; andmeta_learning_avatar::GaussianAvatarModelis the firstMetaModelimplementation over an actual Gaussian avatar — the only prior implementation was aLinearModeltoy never connected to what the crate trains. -
oxigaf-bridge— pure-Rust ingest replaces the Python fallback. A newpicklemodule implements a non-executing Python pickle reader (protocols 0–5:GLOBAL/REDUCE/NEWOBJ/BUILDall produce inert data records, nothing is resolved or called) backingconvert_pytorch_checkpointandconvert_flame_model. The oldscripts/convert_*.pyremain as a documented reference/escape hatch, but nothing in the pipeline needs Python, PyTorch, NumPy, or SciPy for this step anymore. Separately,GafLayerMapperstopped relying on a hardcoded, enumerated U-Net/VAE/CLIP/Upsampler layer table that didn’t matchDiffusionConfig::default(), in favor of direct/↔.path substitution. -
oxigaf-flame— the heat method is now the heat method.heat_geodesicimplements the actual Crane/Weischedel/Wardetzky (2013) algorithm — solving(M + t·Lc)u = δ_sourcevia Jacobi-preconditioned CG, normalizing∇u, then a Poisson solve — replacing what its own 0.1.1 doc comment called “a simplified approximation, not the full heat method.”geodesic_centerswitches its default from an exhaustive search (minutes on a 5,023-vertex FLAME head) to farthest-point sampling overDEFAULT_CENTER_SAMPLES(64) candidates.
Getting Started
cargo add oxigaf
Convert a raw PyTorch checkpoint directly — no torch.load, no Python:
use oxigaf_bridge::convert_pytorch_checkpoint;
use std::path::Path;
fn main() -> anyhow::Result<()> {
// Splits tensors into unet/vae/clip/other by prefix and writes each
// non-empty group as <component>.safetensors
let report = convert_pytorch_checkpoint(
Path::new("checkpoint.pt"),
Path::new("weights/"),
None, // target_dtype: keep the source precision
)?;
println!(
"wrote {} tensors across {} components",
report.total_tensors(),
report.components.len()
);
Ok(())
}
Or drive training and export from the CLI:
# Train with the now-correct photometric gradient
oxigaf train --config experiment.toml
# Export to spec-conformant glTF 2.0
oxigaf export --format gltf --input checkpoint.safetensors --output avatar.gltf
What’s New in 0.1.2
- Two critical backward-pass gradient fixes — the hardcoded-loss bug and the missing SH-color position gradient, both affecting every 0.1.1 training run.
- Two more backward-shader fixes — wrong-Gaussian gradient attribution (WGSL workgroup-uniformity bug) and NaN gradients for culled Gaussians.
- Regularization and SDS gradients wired in for real, plus background-alpha gradient contribution previously missing from
rasterize_bwd.wgsl. - Pure-Rust
.pt/.pklingest —convert_pytorch_checkpoint,convert_flame_model, and a non-executing pickle reader inoxigaf-bridge. - Spec-conformant glTF 2.0 export — one
gltfmodule replacing three incompatible emitters. - Meta-learning avatar model —
GaussianAvatarModel, the first realMetaModelover an actual Gaussian avatar. - Declarative LR schedules and gradient clipping —
LrScheduleConfig,GradientClipConfig, plusGaussianPrunerpruning utilities. - GPU-side pass profiling and device-limit validation in
oxigaf-render. - Sovereignty: C Oniguruma removed (via
oxicandle-core), then OpenSSL andringremoved from the CLI’s HTTP stack (hf-hubdropped entirely). - PLY
f_rest_*property order fixed to channel-major forsh_degree >= 1models, matching the reference 3DGS Python convention.
Tips
- If you trained on 0.1.1, retrain. There is no way to recover the missing SH-color gradient signal, or the correctly-weighted photometric loss, from an already-trained model — this is the one genuinely unavoidable migration cost in this release.
- Re-export any
.plywithsh_degree >= 1written before 0.1.2. Thef_rest_*property order changed to channel-major; older files load with correctly-valued but permuted higher-order SH coefficients. - Reach for
convert_pytorch_checkpoint/convert_flame_modelinstead of the Python scripts — they’re now the documented reference/escape hatch, not the primary path. - Declare LR schedules and clipping in
TrainingConfigviaLrScheduleConfig/GradientClipConfiginstead of hand-wiring the scheduler — both carry#[serde(default)], so existing TOML/JSON configs keep deserializing unchanged. flash_attentionis opt-in now, not implied. If you relied onoxigaf-diffusion’s olddefault = ["flash_attention"], add--features flash_attention(or thefull_performance/all_featuresbundles) explicitly.- macOS: move
~/.cache/oxigafif you have state there.setup/doctor/cachenow agree on~/Library/Caches/oxigaf(dirs::cache_dir()); setOXIGAF_CACHE_DIRto keep the old location instead.
This is the foundation
OxiGAF 0.1.2 leans further into the COOLJAPAN ecosystem it’s built on: oxicandle-core/oxicandle-nn for the diffusion backbone, oxitls-rustcrypto-provider and ureq/rustls for a pure-Rust HTTP stack, oxiarc-archive for archives, and the ToRSh tensor bridge (torsh-core/torsh-tensor/torsh-nn, bumped 0.1.2 → 0.2.0 this release) via oxigaf-bridge. It pairs naturally with oxihuman for a full body-plus-photoreal-face digital human, pipes reconstructed avatars through oximedia for real-time video, and stays Pure Rust on the same foundations as SciRS2, OxiBLAS, and OxiFFT.
Repository: https://github.com/cool-japan/oxigaf
Star the repo if you want digital-human training you can actually trust the gradients of.
The era of a loss function that logs one thing and optimizes another is over. Pure Rust Gaussian avatars are here — and now they train on the objective you actually configured.
— KitaSan at COOLJAPAN OÜ August 28, 2026