Compare commits

...
27 Commits
Author SHA1 Message Date
Jonathan Swartz fb5a6cc995 NanoVDB: remove multi-GPU diagnostics now that the race is fixed
Drop the per-device stripe/voxel-count fprintf in DistributedPointsToGrid and
the "--repeat until-fail:20" mgpu loop in the CodeBuild buildspec. Both were
temporary scaffolding for diagnosing the shared-tile race, which is now fixed
and covered by dedicated regression tests.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-07 15:45:12 +12:00
Jonathan Swartz 76c1643ea7 NanoVDB: drop FewerVoxelsThanDevices multi-GPU test
The test exercised inputCount < deviceCount, which triggers a pre-existing
limitation in radixSortAsync (the initial per-device sort runs on a zero-count
segment and CUB returns "invalid device ordinal"). That path is unrelated to
the shared-tile ownership fix, and the empty interior-stripe behavior the fix
introduces is already covered by SingleUpperNode and MatchesSingleGpu.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-07 15:40:23 +12:00
Jonathan Swartz de2ab1104c NanoVDB: add multi-GPU regression tests for shared-tile ownership
Add deterministic tests covering the DistributedPointsToGrid path where a
single upper-node tile is split across all devices:

- SingleUpperNode: many voxels within one 4096^3 tile spanning many
  lower/leaf nodes; asserts the exact active-voxel count so a reintroduced
  cross-device race is caught deterministically rather than probabilistically.
- MatchesSingleGpu: cross-checks topology and voxel occupancy against the
  trusted single-GPU voxelsToGrid builder on a tile-splitting input.
- FewerVoxelsThanDevices: guards the empty-stripe edge case.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-07 14:23:44 +12:00
Jonathan Swartz 67ff8ca74a NanoVDB: assign each upper-node tile to a single GPU in DistributedPointsToGrid
The distributed builder distributes work by snapping device boundaries to
upper-node (tile) boundaries, since each tile subtree is counted and built
independently on one device. The previous pairwise rebalance could not
consolidate a tile whose points span three or more devices (a fully-interior
device lies entirely within the tile), so such a tile stayed split. Multiple
devices then concurrently registered and cleared/set the same leaf's value
mask with no cross-device ordering, intermittently losing updates and
undercounting active voxels (e.g. the single dense-leaf case collapsing to
one device's stripe).

Replace the pairwise TransformReduce + rebalance kernels with a global,
monotonic host-side boundary snap over the globally-sorted keys. Every tile
is now owned by exactly one device (interior devices may be left empty),
eliminating cross-device writes to shared nodes. Remove the now-unused
EqualityIndicator, left/rightRebalanceKernel, and the transformReduce/rebalance
event arrays.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-07 14:20:03 +12:00
Mark HarrisandClaude Fable 5 b3d4644717 NanoVDB: report per-device counts after the distributed count phase
Diagnostic for the intermittent DenseLeaf undercount, whose failing
value is exactly one device's quarter of the voxels. The counts are
printed once they are final -- after the full per-device stream
synchronize -- so a failing run shows directly whether a stripe was
already lost upstream of aggregation or survived counting and was lost
downstream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Mark Harris <mharris@nvidia.com>
2026-08-07 12:14:22 +12:00
Mark HarrisandClaude Fable 5 5ac55386a6 NanoVDB CI: repeat the mgpu suite until failure
The remaining DenseLeaf undercount is an intermittent race -- the same
code failed and passed clang-Debug on consecutive days -- so a single
green run says nothing. Run the mgpu tests up to twenty times after the
main pass, stopping at the first failure; at seventeen seconds per
iteration the worst case adds about six minutes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Mark Harris <mharris@nvidia.com>
2026-08-07 12:14:22 +12:00
Jonathan Swartz d614dce461 IOMMU checks
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-06 15:35:27 +12:00
Jonathan Swartz dd2526e024 Pin fix
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-06 14:50:59 +12:00
Jonathan Swartz 196dbf3c55 Upload artifacts directly to Github
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-06 14:27:13 +12:00
Jonathan Swartz 4921d36f85 mGPU Diagnostics
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-06 13:30:34 +12:00
Jonathan Swartz 27f141ec4b Fix checkout and statements to refelct pull_request_target
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-06 08:49:33 +12:00
Jonathan Swartz 6d7ff5fa2f job cleanup
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 16:55:11 +12:00
Jonathan Swartz 72c0dd00bf test(nanovdb): expand CodeBuild GPU coverage
Run the NanoVDB Python suite on GPU workers and expose the selected CodeBuild project and fleet configuration for easier CI diagnosis.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 16:29:27 +12:00
Jonathan Swartz 46b9b8287f fix(nanovdb): synchronize distributed grid initialization
Record the root initialization event on its CUDA stream to prevent dependent GPU work from racing, and improve active voxel count diagnostics.

Moved buildspec inside ci/

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 16:05:26 +12:00
Jonathan Swartz 76fd981d42 Changed ManyTiles_CudaPointsToGrid to:
Generate 64 deterministic upper-node tiles.
Put 64 voxels in each tile.
Still trigger the >= 32 segmented-sort path.
Assert exactly 64 upper nodes were created.
Reduce estimated node memory to about 18.6 MiB.

Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 15:33:53 +12:00
Jonathan Swartz 61b0faedb1 Upgrade GH actions versions
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 15:31:32 +12:00
Jonathan Swartz 5b5677485e NanoVDB CI fixes
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 15:04:10 +12:00
Jonathan Swartz 8a527f2811 Full NanoVDB Tests
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 14:43:22 +12:00
Jonathan Swartz 52adef1e28 nvidia-smi topo
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 14:28:21 +12:00
Jonathan Swartz fd2a48e5e8 Switch to 2026 image; add tree install
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 14:10:17 +12:00
Jonathan Swartz 9ae31a5f6c Fix buildspec
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 13:59:08 +12:00
Jonathan Swartz 026b9aaa69 Fix buildspec
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 13:55:22 +12:00
Jonathan Swartz 20ce2193dd Fix project name
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 13:31:47 +12:00
Jonathan Swartz dbb6e04ea9 Add a buildspec with trivial work
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 13:24:16 +12:00
Jonathan Swartz dbbc3cf484 Upgrade credentials version
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 13:17:59 +12:00
Jonathan Swartz 30a86c410f Add push trigger
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 13:16:16 +12:00
Jonathan Swartz 612afc8f26 Add GPU workflow
Signed-off-by: Jonathan Swartz <jonathan@jswartz.info>
2026-08-05 13:14:33 +12:00
5 changed files with 505 additions and 121 deletions
+107 -20
View File
@@ -17,7 +17,7 @@ on:
- 'fvdb/**'
- 'pendingchanges/**'
- '**.md'
pull_request:
pull_request_target:
# Add 'ready_for_review' to run CI when a draft PR is marked ready.
types: [opened, synchronize, reopened, ready_for_review]
branches:
@@ -49,26 +49,30 @@ jobs:
# push OR non-draft PR OR matching manual run
if: |
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'linux')))
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-22.04-8c-32g-300h') || 'ubuntu-latest' }}
name: >
linux-nanovdb:cxx:${{ matrix.config.cxx }}-${{ matrix.config.build }}
# Build in the same image used by the AWS CodeBuild GPU runner so the
# uploaded build tree is binary/runtime compatible when executed there.
container:
image: aswf/ci-openvdb:${{ matrix.config.image }}
image: aswf/ci-openvdb:2024-clang17.2
env:
CXX: ${{ matrix.config.cxx }}
strategy:
matrix:
config:
- { cxx: g++, image: '2024-clang17.2', build: 'Release' }
- { cxx: g++, image: '2024-clang17.2', build: 'Debug' }
- { cxx: clang++, image: '2024-clang17.2', build: 'Release' }
- { cxx: clang++, image: '2024-clang17.2', build: 'Debug' }
- { cxx: g++, label: gcc, build: 'Release' }
- { cxx: g++, label: gcc, build: 'Debug' }
- { cxx: clang++, label: clang, build: 'Release' }
- { cxx: clang++, label: clang, build: 'Debug' }
fail-fast: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
- name: setup_cuda_12
run: |
echo "/usr/local/cuda-12/bin" >> $GITHUB_PATH
@@ -78,26 +82,103 @@ jobs:
- name: build
# NOTE: CMAKE_POSITION_INDEPENDENT_CODE set to fix default behaviour change in clang 14
# https://github.com/AcademySoftwareFoundation/aswf-docker/issues/228
# NOTE: CUDA architecture 86 targets the AWS CodeBuild A10G GPUs used by
# the linux-nanovdb-gpu test stage below.
run: >
./ci/build.sh -v
--build-type=${{ matrix.config.build }}
--components=core,nano,nanotest,nanoexam,nanobench,nanotool,nanopython,nanopytest
--cargs=\'
-DUSE_EXPLICIT_INSTANTIATION=OFF
-DOPENVDB_CORE_STATIC=OFF
-DNANOVDB_USE_CUDA=ON
-DCMAKE_CUDA_ARCHITECTURES="80"
-DCMAKE_CUDA_ARCHITECTURES="86"
-DNANOVDB_USE_OPENVDB=ON
-DCMAKE_INSTALL_PREFIX=`pwd`
-DUSE_BLOSC=ON
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
\'
# Run the non-GPU tests here. GPU-only (cuda/mgpu) tests are executed in
# the linux-nanovdb-gpu job. This step also generates the shared test
# data files (e.g. data/*.nvdb) that the GPU tests consume, so it must
# run before the build tree is uploaded.
- name: test
run: cd build && sudo ctest -V -E ".*cuda.*|.*mgpu.*"
run: cd build && ctest -V -E ".*cuda.*|.*mgpu.*"
- name: package_gpu_build
# Record the absolute workspace path so the GPU runner can restore the
# build tree to the same location, keeping RPATHs and CTest-generated
# absolute paths valid.
run: |
META="$GITHUB_WORKSPACE/nanovdb-gpu-meta.env"
echo "BUILD_WORKSPACE=$GITHUB_WORKSPACE" > "$META"
echo "GITHUB_SHA=$GITHUB_SHA" >> "$META"
echo "CONFIG=${{ matrix.config.label }}-${{ matrix.config.build }}" >> "$META"
cat "$META"
- name: upload_gpu_build
uses: actions/upload-artifact@v7
with:
name: nanovdb-gpu-build-${{ matrix.config.label }}-${{ matrix.config.build }}
path: |
build
nanovdb/nanovdb/python/test/TestNanoVDB.py
nanovdb-gpu-meta.env
retention-days: 1
if-no-files-found: error
linux-nanovdb-gpu:
# Execute the CUDA / multi-GPU tests on AWS CodeBuild (4x A10G) using the
# prebuilt artifacts from linux-nanovdb. Repository secrets are not shared
# with fork PRs, so restrict this job to same-repo events.
if: |
github.repository == 'AcademySoftwareFoundation/openvdb' &&
(github.event_name == 'push' ||
(github.event_name == 'pull_request_target' &&
github.event.pull_request.draft == false &&
github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'linux')))
needs: linux-nanovdb
runs-on: ubuntu-latest
name: >
linux-nanovdb-gpu:${{ matrix.config.label }}-${{ matrix.config.build }}
# GITHUB_TOKEN needs actions:read to fetch the workflow-run artifacts.
permissions:
contents: read
actions: read
strategy:
matrix:
config:
- { label: gcc, build: 'Release' }
- { label: gcc, build: 'Debug' }
- { label: clang, build: 'Release' }
- { label: clang, build: 'Debug' }
fail-fast: false
env:
# Consumed by buildspec.yml on the CodeBuild side.
ARTIFACT_NAME: nanovdb-gpu-build-${{ matrix.config.label }}-${{ matrix.config.build }}
GH_TOKEN: ${{ github.token }}
steps:
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v6
with:
aws-access-key-id: ${{ secrets.CODEBUILD_ID }}
aws-secret-access-key: ${{ secrets.CODEBUILD_SECRET }}
aws-region: us-west-2
- name: Run CodeBuild GPU tests
uses: aws-actions/aws-codebuild-run-build@v1
with:
project-name: OpenVDB
buildspec-override: ci/buildspec_nanovdb.yml
image-override: aswf/ci-openvdb:2024-clang17.2
# GITHUB_* variables (GITHUB_RUN_ID, GITHUB_REPOSITORY, ...) are
# forwarded automatically. The job-scoped Actions runtime values let
# CodeBuild upload report.tgz directly to this workflow run.
env-vars-for-codebuild: ARTIFACT_NAME,GH_TOKEN,ACTIONS_RUNTIME_TOKEN,ACTIONS_RESULTS_URL
windows-nanovdb:
if: |
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'win')))
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'windows-2022-8c-32g-300h') || 'windows-latest' }}
@@ -108,7 +189,9 @@ jobs:
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
- name: path
run: |
# note: system path must be modified in a previous step to it's use
@@ -145,7 +228,7 @@ jobs:
macos-nanovdb:
if: |
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'mac')))
runs-on: ${{ matrix.config.runner }}
@@ -158,7 +241,9 @@ jobs:
- { runner: 'macos-14', cxx: 'clang++', build: 'Debug' }
fail-fast: false
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
- name: install_deps
run: |
./ci/install_macos.sh
@@ -175,21 +260,23 @@ jobs:
nanovdb-lite:
if: |
(github.event_name == 'push' ||
(github.event_name == 'pull_request' && github.event.pull_request.draft == false) ||
(github.event_name == 'pull_request_target' && github.event.pull_request.draft == false) ||
(github.event_name == 'workflow_dispatch' &&
(github.event.inputs.type == 'all' || github.event.inputs.type == 'linux')))
runs-on: ${{ (github.repository_owner == 'AcademySoftwareFoundation' && 'ubuntu-22.04-8c-32g-300h') || 'ubuntu-latest' }}
container:
image: aswf/ci-openvdb:2024-clang17.2
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.head.sha || github.sha }}
- name: install_gtest
run: ./ci/install_gtest.sh latest
- name: build_and_test
run: |
cd nanovdb/nanovdb
sudo mkdir .build
mkdir .build
cd .build
sudo cmake -DUSE_EXPLICIT_INSTANTIATION=OFF -DNANOVDB_BUILD_UNITTESTS=ON -DNANOVDB_USE_OPENVDB=OFF -DNANOVDB_USE_CUDA=OFF ../
sudo make -j8 install
sudo ctest -V
cmake -DUSE_EXPLICIT_INSTANTIATION=OFF -DNANOVDB_BUILD_UNITTESTS=ON -DNANOVDB_USE_OPENVDB=OFF -DNANOVDB_USE_CUDA=OFF -DCMAKE_INSTALL_PREFIX="$(pwd)/install" ../
make -j8 install
ctest -V
+208
View File
@@ -0,0 +1,208 @@
version: 0.2
# Runs the NanoVDB CUDA / multi-GPU unit tests on AWS CodeBuild (4x A10G).
# This spec does NOT build the project. Instead it downloads the prebuilt
# build tree produced by the matching GitHub Actions run (see the
# linux-nanovdb / linux-nanovdb-gpu jobs in .github/workflows/nanovdb.yml),
# restores it at its original absolute path, and executes the GPU and Python tests.
#
# Required environment variables (provided by aws-codebuild-run-build):
# ARTIFACT_NAME - name of the GitHub Actions artifact to fetch
# GH_TOKEN - ephemeral GITHUB_TOKEN with actions:read
# GITHUB_REPOSITORY - owner/repo (auto-forwarded GITHUB_* variable)
# GITHUB_RUN_ID - workflow run that produced the artifact (auto-forwarded)
# ACTIONS_RUNTIME_TOKEN - job-scoped token used to upload the report
# ACTIONS_RESULTS_URL - GitHub Actions artifact service endpoint
env:
variables:
CUDA_HOME: /usr/local/cuda-12
DIAGNOSTIC_GIST_ID: c082e01bf80cd88d045e1e61bd78d43c
DIAGNOSTIC_GIST_REVISION: dcd46d4e3a65c5d36e1cde96ade0c102464d8a45
phases:
install:
commands:
# Rocky Linux 8 image: tools required to fetch and unpack artifacts and
# clone the multi-GPU diagnostic gist.
- dnf install -y jq unzip tree 'dnf-command(config-manager)'
- dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
- dnf install -y gh
- dnf module reset -y nodejs
- dnf module enable -y nodejs:20
- dnf install -y nodejs
- npm install --prefix /tmp/actions-artifact-uploader --omit=dev --no-audit --no-fund @actions/artifact@6.2.1
pre_build:
commands:
# Confirm the GPUs are visible (expect 4x A10G).
- nvidia-smi
- nvidia-smi topo -m
- |
echo "=== IOMMU diagnostics ==="
kernel_cmdline=$(cat /proc/cmdline)
echo "Kernel command line:"
echo "${kernel_cmdline}"
iommu_args=$(echo "${kernel_cmdline}" |
grep -Eo '(^|[[:space:]])(iommu|amd_iommu|intel_iommu)=[^[:space:]]+' |
xargs || true)
if [ -n "${iommu_args}" ]; then
echo "Explicit IOMMU kernel arguments: ${iommu_args}"
else
echo "No explicit IOMMU kernel arguments found"
fi
if dmesg_output=$(dmesg 2>&1); then
echo "IOMMU kernel messages:"
echo "${dmesg_output}" |
grep -Ei 'DMAR|IOMMU|AMD-Vi|amd_iommu|intel_iommu' ||
echo "No IOMMU kernel messages found"
echo "IOMMU passthrough messages:"
echo "${dmesg_output}" |
grep -Ei 'iommu.*(pt|passthrough)|default domain type.*passthrough' ||
echo "No IOMMU passthrough messages found"
else
echo "dmesg unavailable in this container: ${dmesg_output}"
fi
if compgen -G '/sys/kernel/iommu_groups/*/devices/*' >/dev/null; then
echo "Populated IOMMU groups are visible"
group_count=0
typed_group_count=0
for group in /sys/kernel/iommu_groups/*; do
[ -d "${group}" ] || continue
group_count=$((group_count + 1))
if [ -r "${group}/type" ]; then
typed_group_count=$((typed_group_count + 1))
echo "IOMMU group ${group##*/} type: $(cat "${group}/type")"
fi
done
echo "Visible IOMMU group count: ${group_count}"
if [ "${typed_group_count}" -eq 0 ]; then
echo "Per-group IOMMU domain types are not exposed by this kernel"
fi
else
echo "No populated IOMMU groups are visible"
fi
if compgen -G '/sys/class/iommu/*' >/dev/null; then
echo "IOMMU devices visible in /sys/class/iommu:"
for iommu in /sys/class/iommu/*; do
echo " ${iommu##*/}"
done
else
echo "No IOMMU devices are visible in /sys/class/iommu"
fi
# Locate, download and restore the prebuilt build tree.
- 'echo "Fetching artifact ${ARTIFACT_NAME} from run ${GITHUB_RUN_ID}"'
- |
set -euo pipefail
api="https://api.github.com/repos/${GITHUB_REPOSITORY}"
artifacts_json=$(curl -fsSL \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"${api}/actions/runs/${GITHUB_RUN_ID}/artifacts?per_page=100")
count=$(echo "${artifacts_json}" | jq --arg n "${ARTIFACT_NAME}" \
'[.artifacts[] | select(.name==$n)] | length')
if [ "${count}" != "1" ]; then
echo "ERROR: expected exactly 1 artifact named '${ARTIFACT_NAME}', found ${count}"
exit 1
fi
artifact_id=$(echo "${artifacts_json}" | jq -r --arg n "${ARTIFACT_NAME}" \
'.artifacts[] | select(.name==$n) | .id')
echo "Resolved artifact id ${artifact_id}"
# The API returns a short-lived (60s) redirect to blob storage. Resolve
# the redirect first, then download without the Authorization header so
# it is not forwarded to the storage host.
download_url=$(curl -sS -o /dev/null -w '%{redirect_url}' \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"${api}/actions/artifacts/${artifact_id}/zip")
curl -fsSL -o /tmp/nanovdb-gpu-build.zip "${download_url}"
- rm -rf /tmp/nanovdb-gpu-artifact
- mkdir -p /tmp/nanovdb-gpu-artifact
- unzip -q /tmp/nanovdb-gpu-build.zip -d /tmp/nanovdb-gpu-artifact
- |
set -euo pipefail
set -a
. /tmp/nanovdb-gpu-artifact/nanovdb-gpu-meta.env
set +a
echo "Restoring build tree and Python tests to ${BUILD_WORKSPACE}"
mkdir -p "${BUILD_WORKSPACE}"
rm -rf "${BUILD_WORKSPACE}/build"
mv /tmp/nanovdb-gpu-artifact/build "${BUILD_WORKSPACE}/build"
python_test_dir="${BUILD_WORKSPACE}/nanovdb/nanovdb/python/test"
mkdir -p "${python_test_dir}"
mv /tmp/nanovdb-gpu-artifact/nanovdb/nanovdb/python/test/TestNanoVDB.py \
"${python_test_dir}/TestNanoVDB.py"
build:
commands:
- |
set -euo pipefail
set -a
. /tmp/nanovdb-gpu-artifact/nanovdb-gpu-meta.env
set +a
export PATH="${CUDA_HOME}/bin:${PATH}"
export LD_LIBRARY_PATH="${CUDA_HOME}/lib64:${BUILD_WORKSPACE}/build/openvdb/openvdb:${BUILD_WORKSPACE}/build/nanovdb/nanovdb:${LD_LIBRARY_PATH:-}"
cd "${BUILD_WORKSPACE}/build"
ctest -V -R ".*cuda.*|.*mgpu.*|pytest_nanovdb" --output-on-failure
post_build:
commands:
- |
set -euo pipefail
diagnostic_dir="${CODEBUILD_SRC_DIR}/${DIAGNOSTIC_GIST_ID}"
rm -rf "${diagnostic_dir}" "${CODEBUILD_SRC_DIR}/report.tgz"
cd "${CODEBUILD_SRC_DIR}"
gh gist clone "${DIAGNOSTIC_GIST_ID}"
# Pin the executable diagnostic code rather than trusting the mutable
# head of the public gist on every CI run.
git -C "${diagnostic_dir}" checkout --detach "${DIAGNOSTIC_GIST_REVISION}"
diagnostic_status=0
(
cd "${diagnostic_dir}"
bash ./run_on_runner.sh
) || diagnostic_status=$?
if [ -d "${diagnostic_dir}/report" ]; then
tar czf "${CODEBUILD_SRC_DIR}/report.tgz" \
-C "${diagnostic_dir}" report/
else
echo "ERROR: the diagnostic did not produce report/"
exit 1
fi
# Upload directly into the current GitHub Actions job. These values are
# short-lived and scoped to this job's artifact service.
: "${ACTIONS_RUNTIME_TOKEN:?ACTIONS_RUNTIME_TOKEN was not forwarded}"
: "${ACTIONS_RESULTS_URL:?ACTIONS_RESULTS_URL was not forwarded}"
cat > /tmp/actions-artifact-uploader/upload.mjs <<'EOF'
import {DefaultArtifactClient} from '@actions/artifact'
const reportPath = `${process.env.CODEBUILD_SRC_DIR}/report.tgz`
const artifactName = process.env.ARTIFACT_NAME.replace(
/^nanovdb-gpu-build-/,
'nanovdb-gpu-report-'
)
const client = new DefaultArtifactClient()
const result = await client.uploadArtifact(
artifactName,
[reportPath],
process.env.CODEBUILD_SRC_DIR,
{
retentionDays: 7,
compressionLevel: 0
}
)
console.log(
`Uploaded ${artifactName}: id=${result.id}, size=${result.size}, digest=${result.digest}`
)
EOF
node /tmp/actions-artifact-uploader/upload.mjs
exit "${diagnostic_status}"
@@ -37,21 +37,6 @@ private:
uint32_t* mNodeCounts;
};
/// @brief Indicator functor that returns 1 if the input value matches the member value, 0 otherwise
template <typename T, typename std::enable_if<std::is_integral<T>::value>::type* = nullptr>
struct EqualityIndicator
{
EqualityIndicator(const T* value) : mValue(value) {}
__hostdev__
T operator()(const T& x) const
{
return x == (*mValue);
}
private:
const T* mValue;
};
/// @brief Implements the merge path binary search algorithm in order to find the median across two sorted input key arrays
template<typename KeyIteratorIn>
__device__
@@ -118,34 +103,6 @@ void mergePathKernel(KeyIteratorIn keys1, size_t keys1Count, KeyIteratorIn keys2
mergePath(keys1, keys1Count, keys2, keys2Count, key1Intervals, key2Intervals, intervalIndex);
}
/// @brief Extends or shortens the left end of an array interval
template<typename DistanceIteratorIn, typename CountIteratorOut, typename OffsetIteratorOut>
__global__
void leftRebalanceKernel(DistanceIteratorIn leftDistance, DistanceIteratorIn rightDistance, CountIteratorOut leftCount, OffsetIteratorOut leftOffset)
{
if (*leftDistance < *rightDistance) {
*leftCount -= *leftDistance;
}
else {
*leftCount += *rightDistance;
}
}
/// @brief Extends or shortens the right end of an array interval
template<typename DistanceIteratorIn, typename CountIteratorOut, typename OffsetIteratorOut>
__global__
void rightRebalanceKernel(DistanceIteratorIn leftDistance, DistanceIteratorIn rightDistance, CountIteratorOut rightCount, OffsetIteratorOut rightOffset)
{
if (*leftDistance < *rightDistance) {
*rightCount += *leftDistance;
*rightOffset -= *leftDistance;
}
else {
*rightCount -= *rightDistance;
*rightOffset += *rightDistance;
}
}
} // namespace kernels
// Define utility macro used to call cub functions that use dynamic temporary storage
@@ -560,8 +517,6 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
// to reduce overhead.
std::vector<cudaEvent_t> sortEvents(mDeviceMesh.deviceCount());
std::vector<cudaEvent_t> runLengthEncodeEvents(mDeviceMesh.deviceCount());
std::vector<cudaEvent_t> transformReduceEvents(mDeviceMesh.deviceCount());
std::vector<cudaEvent_t> rebalanceEvents(mDeviceMesh.deviceCount());
std::vector<cudaEvent_t> tilePrefixSumEvents(mDeviceMesh.deviceCount());
std::vector<cudaEvent_t> voxelCountEvents(mDeviceMesh.deviceCount());
std::vector<cudaEvent_t> leafCountEvents(mDeviceMesh.deviceCount());
@@ -572,8 +527,6 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
cudaCheck(cudaSetDevice(deviceId));
cudaEventCreateWithFlags(&sortEvents[deviceId], cudaEventDisableTiming);
cudaEventCreateWithFlags(&runLengthEncodeEvents[deviceId], cudaEventDisableTiming);
cudaEventCreateWithFlags(&transformReduceEvents[deviceId], cudaEventDisableTiming);
cudaEventCreateWithFlags(&rebalanceEvents[deviceId], cudaEventDisableTiming);
cudaEventCreateWithFlags(&tilePrefixSumEvents[deviceId], cudaEventDisableTiming);
cudaEventCreateWithFlags(&voxelCountEvents[deviceId], cudaEventDisableTiming);
cudaEventCreateWithFlags(&leafCountEvents[deviceId], cudaEventDisableTiming);
@@ -626,60 +579,68 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
radixSortAsync(mDeviceMesh, mTempDevicePools, mData->d_keys, mKeys, mData->d_indx, mIndices, coordCount, mIntervals, mStripeOffsets, mStripeCounts, sortEvents.data(), sortEvents.data());
// For each segment of sorted keys on each device, we count how many of the leftmost key occur past the left boundary of the segment. The same is done for the rightmost key with the right boundary of the segment.
auto leftIntervals = mIntervals;
auto rightIntervals = mIntervals + mDeviceMesh.deviceCount();
// Rebalance the device segments so that a device boundary always coincides
// with a change in key value. Because TileKeyFunctor assigns identical keys
// to every point that falls in the same upper-node "tile", this aligns the
// device ownership boundaries with tile boundaries. Downstream construction
// assumes each tile (and therefore each lower node, leaf node, and voxel) is
// owned by exactly one device; if a tile straddled a boundary, multiple
// devices would concurrently build the same leaf and race on its value mask.
//
// A single tile can span three or more devices (e.g. one dense leaf whose
// points are split evenly across the mesh). Adjusting only adjacent pairs of
// boundaries cannot consolidate such a tile because a fully-interior device
// lies entirely within it, so we compute the boundaries globally and
// monotonically. mKeys is globally sorted at this point, so a tile boundary
// is simply a position where mKeys changes. Snapping is performed on the host
// over the (small) set of device boundaries; fully-interior devices are left
// empty, which the rest of the pipeline already handles.
for (const auto& [deviceId, stream] : mDeviceMesh) {
cudaCheck(cudaSetDevice(deviceId));
auto deviceStripeCount = mStripeCounts[deviceId];
auto deviceStripeOffset = mStripeOffsets[deviceId];
uint64_t* deviceInputKeys = mKeys + deviceStripeOffset;
if (deviceId > 0) {
cudaStreamWaitEvent(stream, sortEvents[deviceId - 1]);
EqualityIndicator<uint64_t> indicator(deviceInputKeys - 1);
CUB_LAUNCH(DeviceReduce::TransformReduce, mTempDevicePools[deviceId], stream, deviceInputKeys, rightIntervals + deviceId, deviceStripeCount, ::cuda::std::plus(), indicator, 0);
}
else {
rightIntervals[deviceId] = 0;
}
if (deviceId < static_cast<int>(mDeviceMesh.deviceCount() - 1)) {
cudaStreamWaitEvent(stream, sortEvents[deviceId + 1]);
EqualityIndicator<uint64_t> indicator(deviceInputKeys + deviceStripeCount);
CUB_LAUNCH(DeviceReduce::TransformReduce, mTempDevicePools[deviceId], stream, deviceInputKeys, leftIntervals + deviceId, deviceStripeCount, ::cuda::std::plus(), indicator, 0);
}
else {
leftIntervals[deviceId] = 0;
}
cudaEventRecord(transformReduceEvents[deviceId], stream);
cudaCheck(cudaStreamSynchronize(stream));
}
// Rebalance the segments so that a device segment boundary also corresponds to a change in key value. Effectively, this aligns upper node boundaries with device ownership boundaries.
for (const auto& [deviceId, stream] : mDeviceMesh) {
cudaCheck(cudaSetDevice(deviceId));
if (deviceId > 0)
{
cudaStreamWaitEvent(stream, transformReduceEvents[deviceId - 1]);
kernels::rightRebalanceKernel<<<1, 1, 0, stream>>>(leftIntervals + deviceId - 1, rightIntervals + deviceId, mStripeCounts + deviceId, mStripeOffsets + deviceId);
{
const int deviceCount = static_cast<int>(mDeviceMesh.deviceCount());
const ptrdiff_t keyCount = static_cast<ptrdiff_t>(coordCount);
ptrdiff_t previousBoundary = mStripeOffsets[0]; // device 0 always starts at 0
for (int deviceId = 1; deviceId < deviceCount; ++deviceId) {
ptrdiff_t boundary = mStripeOffsets[deviceId];
if (boundary >= keyCount) {
boundary = keyCount;
} else if (boundary > previousBoundary && mKeys[boundary] == mKeys[boundary - 1]) {
// The even-split boundary falls inside a tile run; find its extent
// and snap to whichever end keeps the boundary closest to the
// even split without crossing the previous boundary.
ptrdiff_t runStart = boundary;
while (runStart > previousBoundary && mKeys[runStart - 1] == mKeys[boundary]) --runStart;
ptrdiff_t runEnd = boundary;
while (runEnd < keyCount && mKeys[runEnd] == mKeys[boundary]) ++runEnd;
if (runStart <= previousBoundary) {
boundary = runEnd; // the run reaches the previous device, give the whole tile away
} else {
boundary = (boundary - runStart <= runEnd - boundary) ? runStart : runEnd;
}
}
if (boundary < previousBoundary) boundary = previousBoundary;
mStripeOffsets[deviceId] = boundary;
previousBoundary = boundary;
}
if (deviceId < static_cast<int>(mDeviceMesh.deviceCount() - 1))
{
cudaStreamWaitEvent(stream, transformReduceEvents[deviceId + 1]);
kernels::leftRebalanceKernel<<<1, 1, 0, stream>>>(leftIntervals + deviceId, rightIntervals + deviceId + 1, mStripeCounts + deviceId, mStripeOffsets + deviceId);
// Recompute the per-device counts from the adjusted, monotonic offsets.
for (int deviceId = 0; deviceId < deviceCount; ++deviceId) {
const ptrdiff_t nextOffset = (deviceId + 1 < deviceCount)
? mStripeOffsets[deviceId + 1]
: keyCount;
mStripeCounts[deviceId] = static_cast<size_t>(nextOffset - mStripeOffsets[deviceId]);
}
cudaEventRecord(rebalanceEvents[deviceId], stream);
}
// Parallel RLE in order to obtain tiles
// Parallel RLE in order to obtain tiles. The device boundaries were finalized
// synchronously on the host above, so no per-device rebalance event is needed.
for (const auto& [deviceId, stream] : mDeviceMesh) {
cudaCheck(cudaSetDevice(deviceId));
cudaCheck(cudaEventSynchronize(rebalanceEvents[deviceId]));
auto deviceStripeCount = mStripeCounts[deviceId];
auto deviceStripeOffset = mStripeOffsets[deviceId];
@@ -837,8 +798,6 @@ void DistributedPointsToGrid<BuildT>::countNodes(const PtrT coords, size_t coord
cudaCheck(cudaSetDevice(deviceId));
cudaEventDestroy(sortEvents[deviceId]);
cudaEventDestroy(runLengthEncodeEvents[deviceId]);
cudaEventDestroy(transformReduceEvents[deviceId]);
cudaEventDestroy(rebalanceEvents[deviceId]);
cudaEventDestroy(tilePrefixSumEvents[deviceId]);
cudaEventDestroy(voxelCountEvents[deviceId]);
cudaEventDestroy(leafCountEvents[deviceId]);
@@ -915,7 +874,7 @@ inline void DistributedPointsToGrid<BuildT>::processGridTreeRoot(const PtrT poin
} else {
cudaCheck(cudaMemsetAsync(dst, 0, GridData::MaxNameSize, stream));
}
cudaEventRecord(processGridTreeRootEvent);
cudaCheck(cudaEventRecord(processGridTreeRootEvent, stream));
for (const auto& [otherDeviceId, otherStream] : mDeviceMesh) {
cudaSetDevice(otherDeviceId);
+119 -1
View File
@@ -10,6 +10,9 @@
#include <cuda_runtime_api.h>
#include <gtest/gtest.h>
#include <thread> // for std::thread
#include <algorithm> // for std::sort, std::unique
#include <cstdint>
#include <vector>
#include <thrust/fill.h>
#include <thrust/universal_vector.h>
@@ -243,7 +246,7 @@ TEST(TestNanoVDBMultiGPU, DenseLeaf_DistributedCudaPointsToGrid_UnifiedBuffer)
EXPECT_TRUE(data);
grid = handle.grid<BuildT>();
EXPECT_TRUE(grid);
EXPECT_TRUE(grid->activeVoxelCount() == 512);
EXPECT_EQ(voxelCount, grid->activeVoxelCount());
EXPECT_EQ(nanovdb::Vec3d(1.0), grid->voxelSize());
cudaCheck(cudaFree(voxels));
@@ -415,3 +418,118 @@ TEST(TestNanoVDBMultiGPU, ManyTiles_DistributedCudaPointsToGrid)
cudaCheck(cudaFree(voxels));
cudaSetDevice(current);
}// ManyTiles_DistributedCudaPointsToGrid
/// @brief Regression test for the multi-GPU shared-tile race. All coordinates
/// fall inside a single upper-node tile (each upper node spans 4096
/// voxels per axis, so ijk in [0, 4095] maps to tile (0,0,0)), yet they
/// populate many lower and leaf nodes. With the default even initial
/// split this one tile is shared across every GPU, so the builder must
/// consolidate it onto a single device. A cross-device race in leaf
/// construction would drop a device's contribution and undercount the
/// active voxels, which the exact-count assertion below catches
/// deterministically.
TEST(TestNanoVDBMultiGPU, SingleUpperNode_DistributedCudaPointsToGrid_UnifiedBuffer)
{
int current = 0;
cudaCheck(cudaGetDevice(&current));
using BufferT = nanovdb::cuda::UnifiedBuffer;
using BuildT = nanovdb::ValueOnIndex;
const size_t inputCount = 1 << 18;// 262144
nanovdb::Coord* voxels = nullptr;
cudaCheck(cudaMallocManaged(&voxels, inputCount * sizeof(nanovdb::Coord)));
std::srand(24680);
auto op = [](){ return rand() % 4096; };// stays within a single upper node
for (size_t i = 0; i < inputCount; ++i)
voxels[i] = nanovdb::Coord(op(), op(), op());
// Deterministic expected active-voxel count (the input may contain duplicates).
std::vector<uint64_t> packed(inputCount);
for (size_t i = 0; i < inputCount; ++i)
packed[i] = (uint64_t(voxels[i][0]) << 24) | (uint64_t(voxels[i][1]) << 12) | uint64_t(voxels[i][2]);
std::sort(packed.begin(), packed.end());
const size_t uniqueCount = static_cast<size_t>(std::unique(packed.begin(), packed.end()) - packed.begin());
nanovdb::cuda::DeviceMesh deviceMesh;
nanovdb::tools::cuda::DistributedPointsToGrid<BuildT> converter(deviceMesh);
auto handle = converter.getHandle(voxels, inputCount);
EXPECT_TRUE(handle.deviceData());
EXPECT_TRUE(handle.deviceGrid<BuildT>());
handle.deviceDownload();
auto *grid = handle.grid<BuildT>();
EXPECT_TRUE(grid);
EXPECT_EQ(nanovdb::Vec3d(1.0), grid->voxelSize());
// The input occupies exactly one upper-node tile, so the shared-tile
// consolidation path is exercised.
EXPECT_EQ(1u, grid->tree().nodeCount(2));
// Every unique input voxel must be active exactly once.
EXPECT_EQ(static_cast<uint64_t>(uniqueCount), grid->activeVoxelCount());
nanovdb::util::forEach(0, inputCount, 1, [&](const nanovdb::util::Range1D &r){
auto acc = grid->getAccessor();
for (size_t i=r.begin(); i!=r.end(); ++i) {
const nanovdb::Coord &ijk = voxels[i];
EXPECT_TRUE(acc.probeLeaf(ijk)!=nullptr);
EXPECT_TRUE(acc.isActive(ijk));
EXPECT_TRUE(acc.getValue(ijk) > 0u);
}
});
cudaCheck(cudaFree(voxels));
cudaSetDevice(current);
}// SingleUpperNode_DistributedCudaPointsToGrid_UnifiedBuffer
/// @brief Cross-checks the distributed builder against the trusted single-GPU
/// PointsToGrid on an input that forces a single tile to be split across
/// devices. Index assignment order may differ between the two builders,
/// so we compare topology (node counts, active-voxel count) and voxel
/// occupancy rather than the ValueOnIndex indices themselves.
TEST(TestNanoVDBMultiGPU, MatchesSingleGpu_DistributedCudaPointsToGrid)
{
int current = 0;
cudaCheck(cudaGetDevice(&current));
using BufferT = nanovdb::cuda::UnifiedBuffer;
using BuildT = nanovdb::ValueOnIndex;
const size_t inputCount = 1 << 17;// 131072, all within a single upper node
nanovdb::Coord* voxels = nullptr;
cudaCheck(cudaMallocManaged(&voxels, inputCount * sizeof(nanovdb::Coord)));
std::srand(1357);
auto op = [](){ return rand() % 4096; };
for (size_t i = 0; i < inputCount; ++i)
voxels[i] = nanovdb::Coord(op(), op(), op());
nanovdb::cuda::DeviceMesh deviceMesh;
nanovdb::tools::cuda::DistributedPointsToGrid<BuildT> converter(deviceMesh);
auto distributedHandle = converter.getHandle(voxels, inputCount);
distributedHandle.deviceDownload();
auto *distributedGrid = distributedHandle.grid<BuildT>();
EXPECT_TRUE(distributedGrid);
cudaSetDevice(current);
auto referenceHandle = nanovdb::tools::cuda::voxelsToGrid<BuildT, nanovdb::Coord*, BufferT>(voxels, inputCount);
referenceHandle.deviceDownload();
auto *referenceGrid = referenceHandle.grid<BuildT>();
EXPECT_TRUE(referenceGrid);
EXPECT_EQ(referenceGrid->activeVoxelCount(), distributedGrid->activeVoxelCount());
EXPECT_EQ(referenceGrid->tree().nodeCount(0), distributedGrid->tree().nodeCount(0));
EXPECT_EQ(referenceGrid->tree().nodeCount(1), distributedGrid->tree().nodeCount(1));
EXPECT_EQ(referenceGrid->tree().nodeCount(2), distributedGrid->tree().nodeCount(2));
nanovdb::util::forEach(0, inputCount, 1, [&](const nanovdb::util::Range1D &r){
auto distributedAcc = distributedGrid->getAccessor();
auto referenceAcc = referenceGrid->getAccessor();
for (size_t i=r.begin(); i!=r.end(); ++i) {
const nanovdb::Coord &ijk = voxels[i];
EXPECT_TRUE(referenceAcc.isActive(ijk));
EXPECT_TRUE(distributedAcc.isActive(ijk));
}
});
cudaCheck(cudaFree(voxels));
cudaSetDevice(current);
}// MatchesSingleGpu_DistributedCudaPointsToGrid
+20 -8
View File
@@ -791,20 +791,31 @@ TEST(TestNanoVDBCUDA, FewTiles_CudaPointsToGrid)
}// FewTiles_CudaPointsToGrid
/// @brief Exercises the segmented sort path (>= 32 tiles) in PointsToGrid.
/// Coordinates in [-8000, 8000] produce 4^3 = 64 upper internal node tiles
/// Coordinates are placed in 4^3 = 64 upper internal node tiles
/// (each upper node covers 4096 voxels per dimension), exceeding the threshold of 32.
/// Each tile contains 4^3 voxels in one leaf, giving every segment multiple keys to sort
/// without creating the very large number of lower nodes produced by sparse random voxels.
TEST(TestNanoVDBCUDA, ManyTiles_CudaPointsToGrid)
{
using BuildT = nanovdb::ValueOnIndex;
const size_t voxelCount = 1 << 20;// 1048576
std::vector<nanovdb::Coord> voxels;
{
voxels.reserve(voxelCount);
std::srand(54321);
const int max = 8000, min = -max;
auto op = [&](){return rand() % (max - min) + min;};
while (voxels.size() < voxelCount) voxels.push_back(nanovdb::Coord(op(), op(), op()));
voxels.reserve(1 << 12);// 64 upper tiles * 64 voxels per tile = 4096
for (int tx = -2; tx < 2; ++tx) {
for (int ty = -2; ty < 2; ++ty) {
for (int tz = -2; tz < 2; ++tz) {
const nanovdb::Coord base(tx * 4096 + 64, ty * 4096 + 64, tz * 4096 + 64);
for (int x = 0; x < 4; ++x) {
for (int y = 0; y < 4; ++y) {
for (int z = 0; z < 4; ++z) {
voxels.emplace_back(base + nanovdb::Coord(x, y, z));
}
}
}
}
}
}
const size_t voxelCount = voxels.size();
ASSERT_EQ(size_t(1 << 12), voxelCount);
nanovdb::Coord* d_coords;
cudaCheck(cudaMalloc(&d_coords, voxels.size() * sizeof(nanovdb::Coord)));
@@ -819,6 +830,7 @@ TEST(TestNanoVDBCUDA, ManyTiles_CudaPointsToGrid)
EXPECT_TRUE(grid);
EXPECT_TRUE(grid->valueCount() > 0);
EXPECT_EQ(nanovdb::Vec3d(1.0), grid->voxelSize());
EXPECT_EQ(64u, grid->tree().nodeCount(2));
nanovdb::util::forEach(voxels, [&](const nanovdb::util::Range1D &r){
auto acc = grid->getAccessor();