Repository navigation
madspace: float32 networks - #274
theoheimel wants to merge 30 commits into
Conversation
DataType becomes {dt_int32, dt_float32, dt_float64, batch_sizes}. float32 is
supported in npy I/O, DLPack import/export, graph JSON and the Python enum.
Tensor views now check that the element type matches the tensor dtype.
Instructions with a 'precision' field in instruction_set.yaml are expanded into <name>_float32 and <name>_float64, each with its own opcode. The FunctionBuilder method <name> dispatches on the dtype of its arguments. Kernel policies are templated on the float type (CpuTypes32, GpuTypes32). - matmul and the activations are available in both precisions (sgemm/dgemm) - new instructions convert_float32 and convert_float64 - device copy/zero/add and gradient allocation handle float32 - activation backward kernels now accumulate into the input gradient - fix the shape check of single (non-batch) inputs from Python
Every SIMD backend gets a float32 family FVec32/IVec32/BVec32 with twice the lanes of the float64 vectors and int32 indices and masks of the same width (NEON 4, AVX2/AVX512Y 8, AVX512 16 lanes), used by SimdTypes32. - vector types carry their lane count, which the views and the foreach loop use instead of the global simd_vec_size - loads and gathers dispatch on a VecTag, as int loads exist in both families - jobs are aligned to and literals padded to the largest lane count
- the contiguous global gradient buffer takes the dtype of the trainable globals, which must all share one dtype - Adam step for float32 parameters - the gradient clipper computes the norm in float64 for float32 gradients - Tensor::cast for a CPU copy with converted element type
MLP takes a precision (float32 or float64) for its weights and evaluation. Its input and output stay float64, with conversions at the boundary, so softmaxes, splines and Jacobians around it are unchanged. Flow, DiscreteFlow and ChannelWeightNetwork forward it as subnet_precision / precision. Tests for float32 MLP training against torch, float32 flows and saving and loading float32 globals.
The backward kernels of gather and select declared the (unused) gradient of their integer index input as a float view, while the runtime allocates it as an int32 tensor. The view dtype check rejected this in the backward pass of float32 flows. Also test the gradients of a flow with float32 subnetworks.
Selects float32 or float64 for the weights and evaluation of all MadNIS networks (flow, discrete flows, channel weight network). One setting for all of them, as the optimizer trains them together.
The float32 matmul calls sgemm_ and sgemv_, which the bundled OpenBLAS did not build.
The precision arguments default to DataType.float64, which pybind11_stubgen can only render with an enum class location.
|
fyi #Should fix
#Minor
|
|
@oliviermattelaer Thanks, I've fixed the issues (except for the gridpack one, since we're not aiming for cross-version compatibility and are still alpha anyways) |
# Conflicts: # .github/gpu_ci/madspace_checks.py # madspace/src/cpu/runtime.cpp # madspace/src/driver/systematics.cpp # madspace/src/gpu/runtime.cu # madspace/src/phasespace/phasespace.cpp # madspace/tests/test_histogram_op.py # madspace/tests/test_mlp.py # madspace/tests/test_simd_layout.py
op_unweight returned the empty indices tensor as float64, which the
dtype-checked view in batch_gather rejected ("view element type does not
match dtype") whenever a generator job had an empty batch.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Replace the ones-vector fill + cublas gemv by one block-per-column reduction kernel (double accumulation, no atomics, no temporary storage). cuBLAS split the float32 gemv into dot + reduce kernels, so this removes about 0.3M of the extra float32 kernel launches per 2000 training batches and the ones fill in both precisions. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
# Conflicts: # madspace/src/compgraphs/instruction.cpp
…7 into feat-madspace-fp32
Add a GPU-backend lowering pass (fuse_conversions) that rewrites
matmul->convert_float64 and the select/rational-quadratic-spline
consumers of float32 network outputs into fused ops (matmul_to_float64,
select_with_float32, rqs_{forward,inverse}_with_float32), plus a fused
backward bias-gradient+convert kernel. The compute graph stays portable;
set MADSPACE_NO_FUSION=1 to disable the pass. This removes most of the
extra kernel launches of float32 networks.
Make float32 the default nn_float_precision in the MG7 run card.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With LeakyReLU, a pre-activation within rounding distance of the kink can flip the gradient of an event between madspace and torch in float32, which Adam turns into weight differences of the order of the learning rate. This made test_training[float32-0.0] fail for a few percent of initializations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Looks like the AI agent is not yet happy about this one (or I just put the level of review too high for this one) Finding numbering follows the original review; the withdrawn race finding (2) is omitted. 1. DataType JSON names renamed, legacy spellings rejected
3. Inconsistent gradient dtype for integer inputs (low, unconfirmed in practice)
Reachability is narrow: it needs, for example, a 4. SIMD mode: env var shadowed, re-launch throws
5.
|
The offsets index * stride were computed in 32 bits and wrapped for tensors spanning more than 2^31 elements, e.g. a large batch of a wide tensor. They are now 64-bit products of the 32-bit index and stride (_mul_epu32, which needs no AVX512DQ). The float64 gathers keep one instruction per vector; the float32 and int32 ones gather the two halves of a vector separately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
matmul prefilled its output with the bias and ran the gemm with beta = 1, while the GPU matmul_to_float64 ran it with beta = 0 and added the bias in its conversion kernel. The two round differently, so the lowered GPU graph was not bitwise equal to the unlowered one (and to MADSPACE_NO_FUSION). Both matmuls now run the gemm with beta = 0 and add the bias in the matmul precision afterwards, on CPU and GPU. The plain matmul stays at two GPU launches, the fused one at two. test_matmul_to_float64 now disables the lowering for its reference, which the GPU runtime would otherwise fuse as well, and requires bitwise equal outputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The backward passes of stack, unstack, pop, batch_cat, batch_split, cat and rqs_reshape gave integer inputs float64 gradients, while backward_batch_foreach allocates int32 ones for the same inputs. An integer input reaching both kinds of op would then be read through a view of the wrong dtype. Integers carry no gradient, so these ops now skip them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GPU lowering pass now also rewrites act(matmul(x, w, b)) for the MLP activations (relu, leaky_relu, elu, gelu, sigmoid, softplus) in both precisions into matmul_<act>, which keeps the matmul result as an output for the backward pass. On the GPU it runs the gemm and one kernel that adds the bias and applies the activation (two launches instead of three per hidden layer). Its backward computes the activation gradient inside the bias gradient reduction, which saves a launch there as well. The activations are now structs of value functions, shared by the element-wise kernels and the fused ones, so that the fused instructions round exactly like the unfused ones. The CPU versions, only reached through an explicit fuse_conversions, use the scalar functions and can differ from the SIMD ones of the element-wise kernels in the last bits for the transcendental activations. custom_op in the instruction set can now name an implementation shared by several instructions, with custom_op_args as its leading template arguments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lowering pass fuses network activations as well as conversions now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GpuTensorView::operator+= was a plain read-modify-write, so the gradient of a broadcast input (batch stride 0) and scatter_add of repeated indices raced between threads. Use atomicAdd for floating-point types. test_float32.py drew from a CPU generator while the default device is the test device; draw on the CPU and move to the device, use the test-device context, and loosen two float32 tolerances that depended on the random draw. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…7 into feat-madspace-fp32
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
@oliviermattelaer Thanks! I didn't change 1, 4, 5. Claude says 8 is not correct because recent clang and Apple clang support this flag. The other issues should be fixed, including the failing GPU tests from main. |
# Conflicts: # madspace/generate_pyi.py # madspace/src/driver/systematics.cpp
|
They are still a couple of (important) issues:
Below this it is less important (so drop them if needed):
|
- CPU random_int allocated a float64 tensor for int32 indices, which the new dtype check rejects; replay-buffer sampling aborted on CPU - GPU tensor views decide at launch whether += must be atomic: only views with a zero stride in a threaded dimension (broadcast gradients) use atomicAdd, per-event gradients use plain stores again - fuse_instructions looks up activation names in a static table instead of building strings for every instruction - test_float32: fresh generator per test, so results don't depend on test order; add a random_int + batch_gather test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@oliviermattelaer thanks, I've fixed the issues |
Adds single-precision support to madspace, mainly so the MadNIS networks can be evaluated and trained in float32. This will enable faster MadNIS training and sampling, and reduce gridpack sizes by a factor 2. In terms of unweighting efficiency, i would not expect a difference, but this will need some investigation especially for longer trainings.
Everything else (including normalizing flow jacobians) remains fp64. However, these changes can also be used as a basis to investigate moving more of phasespace to fp32 in the future.
Stacked on #222 (
fix-madspace-simd).Changes
DataTypeis nowint32 / float32 / float64 / batch_sizes. This is a clean break: graph JSON, the Python enum and npy files use the new names, and npy and DLPack gain<f4/f32 support. Tensor views now check that the element type matches the tensor dtype.precision: [float32, float64]. The generator expands it intoX_float32andX_float64, each with its own opcode. The builder methodXdispatches on the argument dtype, so frontend code doesn't change. A float64 literal passed to a float32 call is converted first.matmul(sgemm/dgemm, and the same on cuBLAS/rocBLAS) and all activations exist in both precisions. Newconvert_float32andconvert_float64instructions are differentiable.MLP(..., precision=...)converts at its boundaries.Flow,DiscreteFlowandChannelWeightNetworkforward this assubnet_precision/precision.madnis.nn_float_precision(float32by default, orfloat64). MG7 MadNIS runs therefore use single-precision networks unless the run card says otherwise.Fixes found along the way
gatherandselectbackward kernels used float views for int gradients. The new dtype check caught this..pyistub generation needs to know whereDataTypelives to render its default arguments.random_intallocated a float64 tensor for its int32 indices, which the new dtype check rejects (replay-buffer sampling aborted on CPU).+=on GPU tensor views raced when many threads accumulate into a broadcast (zero-stride) gradient. Whether a view needsatomicAddis now decided at kernel launch from its strides, so only broadcast gradients are atomic and per-event gradients keep plain stores.GPU launch overhead
float32 networks initially needed extra convert kernels and cuBLAS split-K/dot kernels (training is launch-bound). This PR adds:
fuse_conversions(matmul_to_float64,select_with_float32,rqs_{forward,inverse}_with_float32, fused backward bias-grad+convert). The compute graph stays portable;MADSPACE_NO_FUSION=1disables the pass,op_unweightreturned float64 empty indices for empty batches.Results
Physics: no degradation. 100k MadNIS training batches on one A100, 3 seeds per setting (mean of 3; exact reproducibility is not expected).
RSD, unweighting efficiency and weighted-event throughput agree within the seed-to-seed scatter (ttggg seed 3 is a bad run for both precisions). These numbers are from before the fusion pass.
Speed after fusion (
g g > t t~ g g, 64 hidden nodes, 2000 training batches, single run per GPU, same build):Kernel launches (A100): 2.57M (f64) / 2.88M (f32 unfused) / 2.65M (f32 fused). float32 is now ~2% slower than float64 on A100 and 14% / 40% faster on H200 / A10.
Testing
e+ e- > w+ w-with MadNIS: the cross section is 2.6902468 pb in float64 and 2.6902465 pb in float32.madspace/tests/test_fused_float32.py: fused vs unfused values and gradients, lowering graph structure.test_simd_layout.py::test_global_probability_gradient_is_summed_over_eventsfails on GPU.