642cbc7271ea2cdbca9b4fb6c2834e3368a29350
3
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a22a51372e |
Run the parallel helpers on a pool instead of starting threads per chunk
ParallelFor and ParallelChunks started fresh OS threads on every call, one per chunk, through std::async. There are 57 call sites and several sit inside iterative fits, so one run of the heaviest crystal created 13 497 threads and a 900-frame dataset 7 320. Both now run on a persistent pool. The contracts are unchanged: ParallelChunks keeps the same worker count and the same fixed split, so a reduction sums term for term as before, and ParallelFor keeps stealing per item. Two things in the pool are worth knowing. The caller is one of the hands - it claims its own region's tasks and then waits only on tasks already running - so a region entered from inside another region cannot deadlock at any depth, which a shared-queue pool would. And a task wakes one worker rather than the whole pool: on a large machine notify_all wakes every idle thread to find nothing, once per region, tens of thousands of times a run. Two hand-rolled copies of the same pattern now use it, in FrenchWilson and in the two histogram passes of ComputeAsuGroups. Be clear about what this buys today: nothing measurable. Thread creations drop 13 497 -> 770 and entering a parallel region goes from 1.2-2.2 ms to 112 us, an 11-20x cut, but wall clock on 48 threads is level with before, inside the +-5 % this machine's run-to-run placement is worth. What it removes is a cost that grows with the thread count - measured, entry is linear in it - and the machine this is heading for has four times the threads of the one it was measured on, where the same 335 regions a run would cost about 1.8 s of pure thread creation. ComputeAsuGroups' histogram also changes. It is an nthreads x n_groups table, 936 MB at -N 48 on the heaviest crystal and allocated five times a run, and the prefix over it walked DOWN a column - a 19.5 MB stride, so a cache and TLB miss per step, 234 M of them, serially. Both passes now walk rows and split over group ranges. The counts are integers, so the result is bit-identical. This one is reasoning, not measurement: at 48 threads it sits under this machine's noise and could not be shown either way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a1325637d2 |
Spread the scaling corrections and the space-group search over the cores
Build Packages / build:viewer-tgz:cpu (push) Successful in 19m41s
Build Packages / build:viewer-tgz:cuda (push) Successful in 22m26s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 24m17s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 25m22s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m12s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m41s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 29m1s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m8s
Build Packages / XDS test (durin plugin) (push) Successful in 11m39s
Build Packages / build:rpm (rocky9) (push) Successful in 21m51s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / Build documentation (push) Successful in 1m24s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky8) (push) Successful in 26m38s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 22m0s
Build Packages / DIALS test (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m22s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m14s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m19s
Build Packages / Unit tests (push) Successful in 1h19m35s
Build Packages / build:windows:nocuda (push) Successful in 59m3s
Build Packages / build:windows:cuda (push) Successful in 1h2m17s
Two thirds of a rotation run is one thread. The image loop is not the problem - on the heaviest crystal of the battery it is 1.8 s of 40 - and neither GPU nor CPU is saturated, because while the corrections and the space-group search run there is one core working and 47 idle. Mean occupancy over the whole run: 3.9 of 48. In the correction surfaces (absorption in the goniometer frame, detector-plane modulation, absorption against time and detector position - all one function): the per-cell accumulation, the score reduction and the final apply are now chunked, as are the three loops that assign a full to its cell, one of which spends a sine and a cosine per full de-rotating it into the crystal frame. Two full sorts of four million floats went with them: only the nine bin edges are wanted, so they are selected instead, each selection starting where the last one left off. The per-group pass is deliberately left serial. The terms of one group are spread all over the list, so the only way to give a thread groups of its own is to walk in group order, and that trades a near-sequential read of the fulls for a random one over a few hundred megabytes - the trade that already lost once in the combine kernel. The space-group search scores each candidate rotation by correlating I(h) against I(Rh) over the whole merge. Every operator it can ask about comes from a fixed list and none of them depend on each other, so they are scored up front, in parallel, and the search reads the cache. The scratch that stops a pair being counted twice is now per worker rather than shared. Worker counts are gated on how much work there is, not on how many cores the machine has (ThreadsForWork). Both parallel helpers start a thread per chunk, so a small dataset on a large node would otherwise pay for 48 thread starts to sum a few thousand terms - and this runs on 8-core laptops as well as on this node. Measured on the heaviest crystal, idle machine, two runs each, summed over both passes: those phases go 7.88 s -> 5.19 s. Whole-run wall time is the wrong ruler for it - it moves +-4 s between identical runs. Battery 9m45s -> 9m23s, space group 21/24, no failures; 16 of 24 crystals bit-identical to the previous run and the rest inside the noise floor of running one binary twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6368c00173 |
Decode and accumulate the beam-stop projection on the GPU
The pre-scan decompressed its frames on the host and folded them into a per-pixel projection there. On a 16M-pixel detector that is 60 frames of 72 MB to decompress and 20 bytes per pixel to read and write back per frame - about 40 GB of memory traffic - and it was the whole cost of the phase once the mask was no longer the bottleneck. Only the compressed chunk crosses PCIe now. BSLZ4DecoderGPU already exposes the raw decoded bytes (Decode(), the path its own tests use), which is what this needs: the projection is defined on the RAW STORED COUNTS with the pixel type's sentinel skipped, not on the preprocessed image, so nothing here goes through the preprocessor. Sums, maxima and counts are integers, so the device result is identical to the host's rather than merely close. Frames are folded in batches of four. The fold reads and writes the whole accumulator whatever the batch holds, so per frame it was spending most of the bandwidth on the accumulator rather than on the data; four is where that stops mattering, and every frame beyond it is another full frame of device memory, which costs more in cudaMalloc - device-synchronizing - than it saves. The accumulator is built on a thread of its own. It allocates and clears several hundred megabytes, and doing that in the constructor stalled the caller before it had read its first frame. Frames the device cannot take - anything but bitshuffle+LZ4 - still go to a host shard, so a run mixing compressions needs no second code path, and a build without CUDA is unchanged. RotationScaleMergeGPU set the CUDA device in its constructor and never put it back. CUDA's current device is per-thread, so that silently re-pinned the calling thread for the rest of its life, and the destructor freed several gigabytes against whatever device happened to be current by then - CudaDevicePtr records no device of its own. Every entry point now sets the device on entry and restores it on exit. ParallelFor/ParallelChunks moved to common/ParallelFor.h; two files had copies and a third wants them. Measured on a 16M-pixel rotation dataset: pre-scan 4.78 s -> 2.37 s -> ~2.0 s, shadow unchanged at 139126 pixels (22143 on a 2M-pixel dataset). Full 24-crystal battery: same space group on all 24, none failed, 15m32s -> 14m49s. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |