cint

particle to grid transfer in checked integer arithmetic

Harriett Little. Published 2026-10-06.
The program in this page runs in your browser on cint_ref, the reference. Edit it and run it again.

Particle methods for continuum physics, the material point method and its affine descendants, move mass and momentum from particles onto a grid, solve there, and move the result back. The scatter onto the grid is a sum, and when the sum is taken in floating point its result depends on the order the terms arrive in. On a GPU the terms arrive in whatever order the hardware schedules, so two runs of the same program on the same data give grids that differ in their last bits, and the difference grows from there. The usual remedies are sorting the particles so the order is fixed, or accumulating in fixed point. This page takes the second remedy to its end: the transfer step written in cint, where every quantity is an integer, the scatter is run in two different particle orders, and the two grids are compared node by node.

where the order gets in

Each particle carries a mass and a velocity, so a momentum, and sits somewhere inside a grid cell. The transfer gives each of the cell's nodes a share of the particle's mass and momentum, weighted by how close the particle is to that node, and a node collects shares from every particle near it. In floating point the share is a product that is rounded, and the node's total is a sum of rounded terms that is rounded again after each addition, so the total depends on the order of the additions. In parallel code the additions are atomic operations whose order is not specified, which is why the same run twice does not give the same bits. None of this is a bug in any one program. It is what the arithmetic is.

integers do not care about order

Integer addition is associative and commutative, so a checked sum of integers that completes is the same in every order, and the bounds line in the program, run before any particle is scattered, makes every order complete; if the shares themselves are integers the grid is the same in every order too. The shares are made integers by splitting rather than weighting. For a quantity q and a node fraction f, the far node's share is q times f divided by one, rounded once to the nearest integer, and the near node's share is q minus that. The two shares sum to q by construction, with no rounding to lose. In two dimensions the split is done along x and then each part along y, so a particle's mass lands on its four nodes in four integers that sum to the mass exactly, and the same for each component of momentum. Conservation is then not a property to be checked within a tolerance. It is an identity, and the program prints both sides of it.

the program

Positions are in units of 1/65,536 of a cell on an 8 by 8 grid, masses are 1 to 1,000, velocities are -1,000 to 1,000, and 256 particles are placed by a small generator so that every run sees the same cloud. The first line of output is the bounds analysis as an executable statement: the largest value any node can hold is the particle count times the largest mass times the largest speed, and if that product does not fit I64 the program stops there, before any particle is scattered. The scatter then runs twice, once in particle order and once in a scrambled order, and the second grid is compared with the first. The last lines gather velocity back for one particle from its four nodes, with the division rounded by name.

transfer.ci   Run   the first Run loads Python, about 12 MB, once

via cint_ref (reference written in .py)
stdout

    outcome
    

  

what the output shows

The particles' total mass is 126,536 and their total momentum is (9,985,365, -2,138,879), and the grid's totals are the same three numbers, not close to them. Between the two particle orders, 0 of 81 nodes differ, in mass or in either momentum component. The gather returns the mean velocity of one particle's cell, which is not the particle's own velocity and is not meant to be; it is the quantity the grid holds. Change the 97 and 13 that scramble the order to any odd multiplier and any offset, or reverse the loop, and the count stays at zero, because there is no order in which integers add up differently. The test block at the bottom checks the splitting identity for two thousand values under cint test.

the same scatter in floating point

transfer_float.c is the same scatter written the usual way, in C: each particle's mass and momentum weighted by its distance to each of the four nodes and the products added into the grid, with the same 256 particles from the same generator. It runs the scatter in particle order and in all 128 scrambled orders with an odd multiplier, and compares every grid with the first, bit for bit. Built with gcc 13.3.0 and clang 18.1.3 at -O2 -ffp-contract=off, both give these numbers:

arithmeticpositions, in cellsnodes differing, order 97 and 13orders of 128 matching particle ordergrid momentum, x
cint, integer (this program)multiples of 1/65,5360 of 811289,985,365
C, floatmultiples of 1/65,53672 of 8109,985,364.8214111328
C, doublemultiples of 1/65,5360 of 811289,985,365
C, doublemultiples of 1/65,53572 of 8109,985,364.9999999963

In double the page's own particles show nothing, and that is worth saying plainly. Every position is a whole number of 65,536ths of a cell, so every weight is an exact binary fraction, every product fits in 53 bits and every sum the grid takes is exact, and exact addition does not care about order either. Divide the same positions by 65,535 instead and double differs at 72 of 81 nodes, and in float it differs either way. All of it runs on one thread of one processor, so the orders here are chosen rather than scheduled; on a GPU the hardware picks one per run.

grid nodes that differ from the particle-order grid, in float and in integer, for each of 128 orders 20 40 60 81, all 0 nodes of 81 that differ from particle order 1 65 129 193 255 the order (k * a + 13) % 256, by its odd multiplier a C float cint integer a = 1 only rotates the list: 32 cint integer: 0 in every order a = 1: float 32 of 81, integer 0 of 81 a = 3: float 67 of 81, integer 0 of 81 a = 5: float 68 of 81, integer 0 of 81 a = 7: float 74 of 81, integer 0 of 81 a = 9: float 69 of 81, integer 0 of 81 a = 11: float 74 of 81, integer 0 of 81 a = 13: float 70 of 81, integer 0 of 81 a = 15: float 68 of 81, integer 0 of 81 a = 17: float 74 of 81, integer 0 of 81 a = 19: float 72 of 81, integer 0 of 81 a = 21: float 69 of 81, integer 0 of 81 a = 23: float 73 of 81, integer 0 of 81 a = 25: float 67 of 81, integer 0 of 81 a = 27: float 73 of 81, integer 0 of 81 a = 29: float 70 of 81, integer 0 of 81 a = 31: float 72 of 81, integer 0 of 81 a = 33: float 73 of 81, integer 0 of 81 a = 35: float 73 of 81, integer 0 of 81 a = 37: float 69 of 81, integer 0 of 81 a = 39: float 69 of 81, integer 0 of 81 a = 41: float 66 of 81, integer 0 of 81 a = 43: float 68 of 81, integer 0 of 81 a = 45: float 71 of 81, integer 0 of 81 a = 47: float 70 of 81, integer 0 of 81 a = 49: float 73 of 81, integer 0 of 81 a = 51: float 69 of 81, integer 0 of 81 a = 53: float 72 of 81, integer 0 of 81 a = 55: float 72 of 81, integer 0 of 81 a = 57: float 71 of 81, integer 0 of 81 a = 59: float 72 of 81, integer 0 of 81 a = 61: float 72 of 81, integer 0 of 81 a = 63: float 69 of 81, integer 0 of 81 a = 65: float 67 of 81, integer 0 of 81 a = 67: float 70 of 81, integer 0 of 81 a = 69: float 69 of 81, integer 0 of 81 a = 71: float 71 of 81, integer 0 of 81 a = 73: float 68 of 81, integer 0 of 81 a = 75: float 71 of 81, integer 0 of 81 a = 77: float 73 of 81, integer 0 of 81 a = 79: float 73 of 81, integer 0 of 81 a = 81: float 74 of 81, integer 0 of 81 a = 83: float 72 of 81, integer 0 of 81 a = 85: float 74 of 81, integer 0 of 81 a = 87: float 71 of 81, integer 0 of 81 a = 89: float 69 of 81, integer 0 of 81 a = 91: float 69 of 81, integer 0 of 81 a = 93: float 73 of 81, integer 0 of 81 a = 95: float 68 of 81, integer 0 of 81 a = 97: float 72 of 81, integer 0 of 81 a = 99: float 73 of 81, integer 0 of 81 a = 101: float 68 of 81, integer 0 of 81 a = 103: float 74 of 81, integer 0 of 81 a = 105: float 69 of 81, integer 0 of 81 a = 107: float 69 of 81, integer 0 of 81 a = 109: float 75 of 81, integer 0 of 81 a = 111: float 74 of 81, integer 0 of 81 a = 113: float 71 of 81, integer 0 of 81 a = 115: float 72 of 81, integer 0 of 81 a = 117: float 72 of 81, integer 0 of 81 a = 119: float 69 of 81, integer 0 of 81 a = 121: float 66 of 81, integer 0 of 81 a = 123: float 68 of 81, integer 0 of 81 a = 125: float 69 of 81, integer 0 of 81 a = 127: float 73 of 81, integer 0 of 81 a = 129: float 67 of 81, integer 0 of 81 a = 131: float 71 of 81, integer 0 of 81 a = 133: float 69 of 81, integer 0 of 81 a = 135: float 73 of 81, integer 0 of 81 a = 137: float 71 of 81, integer 0 of 81 a = 139: float 71 of 81, integer 0 of 81 a = 141: float 68 of 81, integer 0 of 81 a = 143: float 74 of 81, integer 0 of 81 a = 145: float 72 of 81, integer 0 of 81 a = 147: float 72 of 81, integer 0 of 81 a = 149: float 71 of 81, integer 0 of 81 a = 151: float 71 of 81, integer 0 of 81 a = 153: float 69 of 81, integer 0 of 81 a = 155: float 71 of 81, integer 0 of 81 a = 157: float 65 of 81, integer 0 of 81 a = 159: float 70 of 81, integer 0 of 81 a = 161: float 71 of 81, integer 0 of 81 a = 163: float 70 of 81, integer 0 of 81 a = 165: float 74 of 81, integer 0 of 81 a = 167: float 72 of 81, integer 0 of 81 a = 169: float 75 of 81, integer 0 of 81 a = 171: float 69 of 81, integer 0 of 81 a = 173: float 71 of 81, integer 0 of 81 a = 175: float 65 of 81, integer 0 of 81 a = 177: float 67 of 81, integer 0 of 81 a = 179: float 72 of 81, integer 0 of 81 a = 181: float 73 of 81, integer 0 of 81 a = 183: float 70 of 81, integer 0 of 81 a = 185: float 70 of 81, integer 0 of 81 a = 187: float 73 of 81, integer 0 of 81 a = 189: float 73 of 81, integer 0 of 81 a = 191: float 73 of 81, integer 0 of 81 a = 193: float 73 of 81, integer 0 of 81 a = 195: float 68 of 81, integer 0 of 81 a = 197: float 74 of 81, integer 0 of 81 a = 199: float 70 of 81, integer 0 of 81 a = 201: float 67 of 81, integer 0 of 81 a = 203: float 73 of 81, integer 0 of 81 a = 205: float 72 of 81, integer 0 of 81 a = 207: float 72 of 81, integer 0 of 81 a = 209: float 68 of 81, integer 0 of 81 a = 211: float 72 of 81, integer 0 of 81 a = 213: float 67 of 81, integer 0 of 81 a = 215: float 70 of 81, integer 0 of 81 a = 217: float 75 of 81, integer 0 of 81 a = 219: float 75 of 81, integer 0 of 81 a = 221: float 73 of 81, integer 0 of 81 a = 223: float 75 of 81, integer 0 of 81 a = 225: float 73 of 81, integer 0 of 81 a = 227: float 70 of 81, integer 0 of 81 a = 229: float 71 of 81, integer 0 of 81 a = 231: float 75 of 81, integer 0 of 81 a = 233: float 70 of 81, integer 0 of 81 a = 235: float 69 of 81, integer 0 of 81 a = 237: float 71 of 81, integer 0 of 81 a = 239: float 76 of 81, integer 0 of 81 a = 241: float 72 of 81, integer 0 of 81 a = 243: float 70 of 81, integer 0 of 81 a = 245: float 71 of 81, integer 0 of 81 a = 247: float 72 of 81, integer 0 of 81 a = 249: float 73 of 81, integer 0 of 81 a = 251: float 69 of 81, integer 0 of 81 a = 253: float 74 of 81, integer 0 of 81 a = 255: float 75 of 81, integer 0 of 81
The same 256 particles scattered in each of the 128 orders (k * a + 13) % 256 with an odd multiplier a, every grid compared bit for bit with the particle-order grid. In float every order changes it: a = 1, which only rotates the particle list, changes 32 nodes, and every other order changes 65 to 76. The integer program changes 0 in every order. Float from transfer_float.c built with -DLIST; integer from the program above with 97 replaced by each a, run in cint_ref.
data
afloat, nodes differinginteger, nodes differing
1320
3670
5680
7740
9690
11740
13700
15680
17740
19720
21690
23730
25670
27730
29700
31720
33730
35730
37690
39690
41660
43680
45710
47700
49730
51690
53720
55720
57710
59720
61720
63690
65670
67700
69690
71710
73680
75710
77730
79730
81740
83720
85740
87710
89690
91690
93730
95680
97720
99730
101680
103740
105690
107690
109750
111740
113710
115720
117720
119690
121660
123680
125690
127730
129670
131710
133690
135730
137710
139710
141680
143740
145720
147720
149710
151710
153690
155710
157650
159700
161710
163700
165740
167720
169750
171690
173710
175650
177670
179720
181730
183700
185700
187730
189730
191730
193730
195680
197740
199700
201670
203730
205720
207720
209680
211720
213670
215700
217750
219750
221730
223750
225730
227700
229710
231750
233700
235690
237710
239760
241720
243700
245710
247720
249730
251690
253740
255750

same bytes from the reference and the compiler

The compiled program writes the same five lines as the reference, byte for byte, and the test passes under both. The one emitted C file was built with four compilers on three machines, each at -O2 (/O2 for MSVC), and run once on each; every build exited with status 0, as a program that completes does:

runmachinestdout SHA-256
cint_refthis page1245bd0e97fc6a41a847d1ee90d0df3267f7bfef7bff05b372dff1b3831fbf07
clang 18.1.3x86-64 Linux1245bd0e97fc6a41a847d1ee90d0df3267f7bfef7bff05b372dff1b3831fbf07
gcc 13.3.0x86-64 Linux1245bd0e97fc6a41a847d1ee90d0df3267f7bfef7bff05b372dff1b3831fbf07
MSVC 19.44x86-64 Windows 111245bd0e97fc6a41a847d1ee90d0df3267f7bfef7bff05b372dff1b3831fbf07
Apple Clang 21.0.0arm64 macOS 271245bd0e97fc6a41a847d1ee90d0df3267f7bfef7bff05b372dff1b3831fbf07

The box below is the C that cintc emitted for the program as shipped, 1,868 lines for 118 lines of cint, most of it the checked arithmetic written out.

cintc (the cint compiler)


what is not claimed

This is the transfer step and nothing else. There are no forces, no constitutive model, no time integration and no grid solve, so it is not a fluid or a granular simulation; it is the two places in one where the order of operations enters, isolated. The weights are linear hat functions, not the quadratic B-splines most material point codes use, and the affine velocity terms of the affine particle-in-cell method are not carried; both would be more splits of the same kind. The program runs on one thread and is not written as a kernel. The compiler now compiles kernels and runs them on the CPU and, through CUDA, on an NVIDIA GPU, but that GPU path is not yet a supported backend, so the parallel case that motivates the paper is not executed here. What is executed is the property the parallel case needs: that the grid does not depend on the order the particles arrive in, shown by arriving in two orders.

limits

The bound in the first line is for the demo's ranges; a real simulation declares its own ranges and the same line checks them. Momentum here is mass times an integer velocity with no further scale; a physical unit system would put a scale on velocity and the products would be wider, and I128 accumulators are available in the reference for that but not yet in the compiler. That floating-point accumulation with atomics on a GPU varies from run to run is the published finding of the GPU vendors cited below, not a measurement made here; what is measured here is the order dependence behind it, on one CPU thread with the orders chosen by hand.

references

cite as

Harriett Little. Particle to grid transfer in checked integer arithmetic. integerc.dev, 2026. https://integerc.dev/papers/mpm/

Harriett Little
October 2026