diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dffc718..f82666c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,8 @@ # Contributing to VRP-GPU -Thank you for contributing. This document describes the repository boundaries -and the checks expected before a pull request. +Thank you for contributing. This document defines the repository boundaries +and evidence required for a pull request. Start at the repository root unless +a command explicitly changes directory. ## Development environments @@ -12,9 +13,19 @@ The repository intentionally has two Cargo workspaces: - `crates/vrp-gpu-kernel` is a standalone, unpublished workspace. It uses the pinned nightly and cuda-oxide toolchain to regenerate `kernel.ptx`. -Do not introduce a root-workspace dependency on the kernel package. Generated -PTX is copied into `crates/vrp-gpu/kernel.ptx` and committed with its source -change. +For CPU algorithms, parsing, documentation and the CUDA **host** code, install +stable Rust with `rustfmt` and `clippy`, plus `just` and `cargo-deny` for the +full gate. The public crate declares MSRV 1.88; test compatibility against +that version when changing public code or dependencies. No CUDA compiler is +needed for CPU changes. + +For kernel source changes, use the nightly pinned in +`crates/vrp-gpu-kernel/rust-toolchain.toml`, the pinned cuda-oxide Git revision, +and the toolchain requirements of that revision. The kernel workspace is +excluded from the stable root workspace. See +[kernel development](docs/kernel-development.md) for setup, regeneration and +hardware checks. Do not add a dependency from the root workspace to the +kernel package. ## Git workflow @@ -25,15 +36,18 @@ change. - `hotfix/`: critical fixes branched from `main`. Use atomic Conventional Commits such as `feat(gpu): ...` or -`docs(release): ...`. Every commit must contain a Developer Certificate of -Origin trailer and a cryptographic signature: +`docs(release): ...`. Write code, documentation, commit messages and PRs in +English. Every commit must contain a Developer Certificate of Origin trailer +and a cryptographic signature: ```sh git commit -s -S ``` -Feature branches merge into `develop` through a pull request using a non-fast- -forward merge. +Feature branches merge into `develop` through a pull request using a +non-fast-forward merge. Keep data-only benchmark records distinct from the +implementation that produced them. Never rewrite another contributor's +published commits to add a signature. ## Quality gates @@ -44,8 +58,14 @@ just ci ``` This checks formatting, all targets and features, Clippy, tests, rustdoc, -package contents and dependency policy. To run parts independently, use -`just --list`. +package contents and dependency policy. `cargo test` skips the existing GPU +hardware tests because they are marked `#[ignore]`; passing `just ci` +alone is not evidence of GPU execution. To run parts independently, use +`just --list`. The package and dependency-policy stages may need network +access for the crates.io index or advisory database. + +Use the [validation matrix](docs/testing.md) to select additional checks for +CPU, GPU, package or documentation changes and to report exactly what ran. Changes to kernel source or the PTX artifact also require: @@ -58,6 +78,7 @@ cmp vrp_gpu_kernel.ptx ../vrp-gpu/kernel.ptx GPU behavior must be checked against the CPU reference on supported hardware. Hardware-only tests stay marked `#[ignore]` so CPU-only CI remains portable. +Regenerate PTX from the pinned source; never edit the checked-in PTX by hand. ## Pull requests @@ -69,4 +90,10 @@ A pull request should: 4. Keep unrelated refactoring in separate commits or pull requests. 5. Avoid performance claims without a reproducible benchmark record. +For a fork that changes public behavior, also state the supported input model, +error and panic behavior, numerical tolerance, and any new hardware assumption. +Tests should compare GPU output with the CPU oracle and, for 2-opt changes, +with a full route-cost recomputation. Do not infer support for another NVIDIA +architecture from the existence of an `sm_120` artifact. + Publication and release steps are documented in [docs/releasing.md](docs/releasing.md). diff --git a/README.md b/README.md index 91d0bae..b8e1b63 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,11 @@ [![CI](https://github.com/pedrozaz/vrp-gpu/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/pedrozaz/vrp-gpu/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE) -VRP-GPU is an experimental Rust library for vehicle-routing heuristics with an -optional CUDA execution path. The CPU implementation is the correctness -reference; versioned PTX kernels accelerate supported local-search operations -when the `gpu` feature is enabled. +VRP-GPU is an experimental Rust library for capacitated vehicle routing (CVRP). +It parses Solomon-style instances, builds routes with a greedy nearest-neighbor +heuristic, and provides CPU 2-opt local search. An optional CUDA path evaluates +2-opt candidates and selects one improving move. The CPU implementation is the +correctness reference for GPU validation. > **Status:** active alpha. The crate has not been published to crates.io yet, > and public APIs may change before the first stable release. @@ -42,7 +43,8 @@ vrp-gpu = { git = "https://github.com/pedrozaz/vrp-gpu", features = ["gpu"] } The default feature set is CPU-only. See the [crate-specific README](crates/vrp-gpu/README.md) for a minimal example and the -public feature contract. +public feature contract. The current internal CLI prints a placeholder message; +use the library API for actual routing work. ## Compatibility @@ -50,14 +52,19 @@ public feature contract. - **CPU path:** does not require CUDA or an NVIDIA GPU. - **GPU path:** currently developed and hardware-validated on Linux with an NVIDIA GeForce RTX 5060 Ti (`sm_120`). Broader hardware support is not yet - claimed. + claimed. A CUDA driver and a device able to load the checked-in PTX are + required at runtime; enabling the Cargo feature alone does not supply them. - **Kernel development:** uses the toolchain pinned in `crates/vrp-gpu-kernel/rust-toolchain.toml` and the pinned cuda-oxide revision. ## Documentation +- [Library usage and API contract](crates/vrp-gpu/README.md) +- [Input model and algorithm semantics](docs/user-guide.md) - [Architecture and publication boundaries](docs/architecture.md) - [Deterministic 2-opt reduction](docs/2opt-gpu-reduction.md) +- [Validation matrix and evidence rules](docs/testing.md) +- [Kernel development and PTX regeneration](docs/kernel-development.md) - [Release and crates.io checklist](docs/releasing.md) - [Benchmark record format](docs/benchmarks/README.md) - [Changelog](CHANGELOG.md) diff --git a/crates/vrp-gpu/README.md b/crates/vrp-gpu/README.md index 5fbb6be..46e9bd7 100644 --- a/crates/vrp-gpu/README.md +++ b/crates/vrp-gpu/README.md @@ -1,8 +1,10 @@ # vrp-gpu -`vrp-gpu` provides vehicle-routing data structures, deterministic CPU reference -heuristics and opt-in CUDA acceleration. The current API parses Solomon-style -CVRP instances, constructs feasible routes and evaluates or selects 2-opt moves. +`vrp-gpu` provides CVRP data structures, CPU reference heuristics and optional +CUDA evaluation of 2-opt moves. It parses Solomon-style text, constructs routes +with a greedy nearest-neighbor heuristic, and improves routes on the CPU. +The GPU API computes candidate deltas or selects a single move; it does not run +a complete GPU search loop or mutate a route. The crate is experimental and has not been published to crates.io yet. @@ -10,21 +12,24 @@ The crate is experimental and has not been published to crates.io yet. | Feature | Default | Effect | | --- | --- | --- | -| `gpu` | No | Enables CUDA driver orchestration through `cudarc` and the embedded `sm_120` PTX artifact | +| `gpu` | No | Exposes `local_search::gpu`, adds `cudarc`, and embeds the checked-in PTX targeting `sm_120` | -Without `gpu`, the library has no third-party runtime dependencies. +Without `gpu`, the library has no third-party runtime dependencies. Enabling +`gpu` requires a working NVIDIA driver when a GPU operation is executed. The +PTX is precompiled; consumers do not need cuda-oxide or a nightly Rust compiler. ## Installation -During development, use the Git repository: +Before publication, use the Git repository and pin a tested revision for a +reproducible application build: ```toml [dependencies] -vrp-gpu = { git = "https://github.com/pedrozaz/vrp-gpu" } +vrp-gpu = { git = "https://github.com/pedrozaz/vrp-gpu", rev = "" } ``` -After the first crates.io release, the dependency will use the normal registry -form: +After an actual crates.io release, use the published version. The following is +an example of the intended form, not a currently available release: ```toml [dependencies] @@ -36,7 +41,11 @@ vrp-gpu = "0.1" ```rust use std::error::Error; -use vrp_gpu::{construct::nearest_neighbor, instance::SolomonInstance}; +use vrp_gpu::{ + construct::nearest_neighbor, + instance::SolomonInstance, + local_search::cpu::two_opt, +}; fn main() -> Result<(), Box> { let input = "\ @@ -49,22 +58,86 @@ CUSTOMER 2 0 1 5 0 100 0 "; let instance: SolomonInstance = input.parse()?; - let solution = nearest_neighbor(&instance); + let mut solution = nearest_neighbor(&instance); assert!(solution.is_feasible(&instance)); + let before = solution.total_distance(&instance); + two_opt(&mut solution, &instance); + assert!(solution.is_feasible(&instance)); + assert!(solution.total_distance(&instance) <= before); + Ok(()) +} +``` + +`nearest_neighbor` panics on malformed instances, a customer whose demand +exceeds vehicle capacity, or exhaustion of the configured fleet. Fleet +exhaustion only means this greedy construction failed. If instance data are +constructed or changed through public fields, call `SolomonInstance::validate` +before passing them to CPU routines. + +To use the optional GPU API, enable the feature in the dependency: + +```toml +[dependencies] +vrp-gpu = { git = "https://github.com/pedrozaz/vrp-gpu", rev = "", features = ["gpu"] } +``` + +Replace `` with the exact Git commit you validated. +`gpu::evaluate_two_opt_deltas` returns a row-major `n × n` matrix with computed +`f32` deltas for valid `i < j` and positive infinity elsewhere. Extreme finite +input distances can still yield non-finite arithmetic results. +`gpu::best_two_opt_move` returns the best finite negative delta as +`Option`. Exact ties choose +the lowest row-major `(i, j)`. Both functions return `GpuEvaluationError` for +invalid input, size overflow or CUDA driver failure. Empty and singleton routes +return without creating a CUDA context. A returned move is only a proposal: +apply it with `cpu::apply_two_opt` if your application accepts it. + +After enabling `gpu`, one selection and application can be written as follows. +The documentation build compiles this example without running it because a +GPU and driver are required: + +```rust,no_run +#[cfg(feature = "gpu")] +fn main() -> Result<(), Box> { + use vrp_gpu::{ + instance::SolomonInstance, + local_search::{cpu, gpu}, + solution::Route, + }; + + let input = r#"DEMO +VEHICLE +1 10 +CUSTOMER +0 0 0 0 0 100 0 +1 1 0 0 0 100 0 +2 0 1 0 0 100 0 +3 2 1 0 0 100 0 +"#; + let instance: SolomonInstance = input.parse()?; + let mut route = Route::from_nodes(vec![1, 2, 3]); + if let Some(best) = gpu::best_two_opt_move(&route, &instance)? { + cpu::apply_two_opt(&mut route, best.i, best.j); + } Ok(()) } + +#[cfg(not(feature = "gpu"))] +fn main() {} ``` -Enable `gpu` to access `local_search::gpu`. GPU entry points validate their -inputs before using CUDA, while empty and singleton routes use CPU-only fast -paths. The current PTX artifact is hardware-validated on an NVIDIA GeForce RTX -5060 Ti (`sm_120`); compatibility with other architectures is not yet claimed. +Each nontrivial GPU call currently creates a CUDA context, loads the embedded +PTX and transfers the matrix and route. There is no reusable device session or +GPU convergence loop. The checked-in artifact targets `sm_120` and has been +hardware-validated on an NVIDIA GeForce RTX 5060 Ti. Other devices have no +compatibility claim. See the [input and algorithm guide](https://github.com/pedrozaz/vrp-gpu/blob/develop/docs/user-guide.md) +for exact model and numerical contracts. ## Minimum supported Rust version -The supported feature set requires Rust 1.88 or newer. Raising the MSRV is a -release-level change and must be recorded in the changelog. +The supported feature set requires Rust 1.88 or newer (edition 2024). Raising +the MSRV is a release-level change and must be recorded in the changelog. See the [repository documentation](https://github.com/pedrozaz/vrp-gpu/tree/develop/docs) for architecture, kernel contracts and release procedures. diff --git a/docs/architecture.md b/docs/architecture.md index 4f1cc8e..75c14ef 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -23,6 +23,11 @@ Only `vrp-gpu` is intended for crates.io. The other packages use project- qualified names to avoid collisions with unrelated crates and are explicitly marked `publish = false`. +`vrp-gpu-cli` is currently a placeholder, not a supported solver command. +`vrp-gpu-bench` is an internal tool whose protocols and results must be +documented under [benchmark records](benchmarks/README.md). Read the +[library guide](user-guide.md) for the public APIs. + ## Runtime boundary The default feature set is CPU-only. Enabling `gpu` adds `cudarc` and exposes @@ -43,6 +48,8 @@ vrp-gpu-kernel (nightly + cuda-oxide, development only) Kernel source and generated PTX must change together. The checked-in artifact is part of the public package and therefore part of its compatibility surface. +The PTX currently declares `.target sm_120`. The package includes this one +artifact; it does not compile PTX for the consumer's GPU at build time. ## Correctness boundary @@ -51,12 +58,30 @@ only after parity tests compare candidate deltas and deterministic selection against that reference. Floating-point comparisons use documented tolerances; tie-breaking uses exact row-major order. +The shared input representation is a row-major `f32` distance matrix of N² +entries plus customer IDs in route order. The implemented 2-opt delta formula +changes two boundary edges and assumes symmetric distances. CPU best-move +selection evaluates O(n²) candidate pairs for a route of n customers, using +O(1) additional selection state. The GPU evaluator materializes n² `f32` +deltas on device; the reduction repeatedly shrinks them to one candidate. +The full-matrix GPU API also downloads n² deltas. These are algorithmic and +allocation bounds, not measured speedup claims. + +The GPU host functions validate the instance and route, create a context, +load the embedded module, transfer inputs, launch kernels and synchronize on +each nontrivial call. There is no persistent context cache or GPU route +convergence API. `best_two_opt_move` returns a proposal; applying it and +repeating search remain the caller's responsibility. + ## Public API boundary Public modules live in `crates/vrp-gpu/src`. Workspace binaries may depend on the library, but the library must not depend on them or on the kernel workspace. New public APIs require rustdoc, tests and a changelog entry. Feature-gated APIs must continue to allow the default CPU-only package to compile without CUDA. +The public model currently covers capacitated routing only. Solomon time-window +and service fields are retained but not enforced by route construction, +feasibility checks or 2-opt. See [model semantics](user-guide.md). ## Package contents @@ -68,3 +93,6 @@ to the archive. Validate the exact archive before a release with: cargo package -p vrp-gpu --list cargo package -p vrp-gpu --locked ``` + +See [kernel development](kernel-development.md) for PTX regeneration and +[testing](testing.md) for the evidence expected of a change. diff --git a/docs/kernel-development.md b/docs/kernel-development.md new file mode 100644 index 0000000..cd97d2c --- /dev/null +++ b/docs/kernel-development.md @@ -0,0 +1,60 @@ +# Kernel development and PTX regeneration + +The public crate embeds `crates/vrp-gpu/kernel.ptx` with `include_str!`. +Consumers compile the stable `vrp-gpu` crate and do not build the separate +kernel workspace. The source of the PTX is +`crates/vrp-gpu-kernel/src/lib.rs`; its `Cargo.toml` pins cuda-oxide to an +exact Git revision and its `rust-toolchain.toml` pins a nightly compiler. + +## Prerequisites + +Work on a machine configured for the pinned cuda-oxide revision. Install the +pinned nightly and its listed components, the `cargo-oxide` subcommand, and +the LLVM/CUDA tools required by that revision. Check the actual revision's +setup instructions and `cargo oxide doctor` before changing a kernel: this +toolchain changes independently of the stable host crate. The current +generator command targets `sm_120`. A successful PTX build is not by itself +proof that a CUDA driver will load or execute the artifact on a given GPU. + +## Regenerate and review + +From the repository root: + +```sh +just kernel-ci +cd crates/vrp-gpu-kernel +cargo oxide doctor +cargo oxide inspect --arch sm_120 +cmp vrp_gpu_kernel.ptx ../vrp-gpu/kernel.ptx +``` + +`cargo oxide inspect` writes `vrp_gpu_kernel.ptx` for this package. The final +`cmp` checks whether the generated artifact matches the committed one. When +the difference is intentional, inspect the generated PTX, copy it to +`../vrp-gpu/kernel.ptx`, rerun `cmp`, and commit kernel source and PTX in the +same PR. Do not copy the similarly named `vrp_kernel.ptx`; that file can be +left over from an earlier package identity. Generated PTX and LLVM +intermediates in the kernel workspace are ignored by Git; the public crate's +PTX is versioned and included in its archive. + +Inspect the resulting `.version`, `.target`, exported entry names, parameter +order/types, launch dimensions, and shared-memory requirements. The host +launch builder in `src/local_search/gpu.rs` must match the actual PTX ABI. +For the reduction kernel, all 256 lanes must reach every barrier, even when +their input offset is out of range; see the detailed +[reduction contract](2opt-gpu-reduction.md). + +## Validate behavior + +Return to the repository root and run `just ci`. On the target hardware, run: + +```sh +cargo test -p vrp-gpu --features gpu -- --ignored --test-threads=1 +``` + +The tests compare GPU deltas and selected moves with CPU references. Record +the GPU model, driver, PTX target, compiler revision and test output. If +memory access or synchronization changed, run the relevant Compute Sanitizer +checks documented in [the reduction contract](2opt-gpu-reduction.md). +Do not claim support for another architecture without regeneration and +hardware validation for that architecture. diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..d80d018 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,53 @@ +# Validation matrix and evidence rules + +Run commands from the repository root unless stated otherwise. Record the +exact commands and results in a PR. CI is portable and cannot establish GPU +behavior without hardware. A command that did not run must be reported as +such, even when an adjacent command passed. + +| Change | Required local evidence | Additional evidence | +| --- | --- | --- | +| Documentation only | `just ci` when commands, examples, package metadata or rustdoc change; check local links and executable examples | Note commands that depend on unavailable hardware or network. | +| CPU/parser/public API | `just ci`; `cargo test --workspace --no-default-features --locked`; `cargo +1.88.0 check -p vrp-gpu --all-features --locked` for API or dependency changes | Add tests for malformed public input, bounds, ties and full-cost agreement where applicable. | +| CUDA host or PTX | All applicable stable-workspace checks; `just kernel-ci` for kernel source; hardware tests with `--ignored` | Record GPU, driver, PTX target and toolchain; use Compute Sanitizer when changing synchronization or memory access. | +| Release/package | `just ci`; inspect `cargo package -p vrp-gpu --list` and `cargo package -p vrp-gpu --locked` from a clean tree | Follow [releasing](releasing.md) before publishing. | + +The current root `just ci` runs `fmt`, all-feature check, strict Clippy, +all-feature tests, rustdoc with warnings denied, crate packaging and +`cargo deny`. `just kernel-ci` changes into the standalone kernel workspace +and runs formatting, check, strict Clippy and tests there. Neither recipe runs +hardware-only ignored tests automatically. + +## GPU correctness protocol + +```sh +cargo test -p vrp-gpu --features gpu -- --ignored --test-threads=1 +``` + +Run this only where an NVIDIA driver and compatible GPU are available. It +runs all ignored tests of that package; review new ignored tests before using +the broad filter. The existing tests compare each valid GPU delta with the CPU +reference using `1e-5 * max(1, abs(cpu_delta))`, verify invalid matrix cells +exactly, and compare selected move indices exactly. Reduction tests also cover +boundary widths, non-finite values, exact ties and repeated runs. These tests +validate the checked-in PTX on the hardware used, not all possible devices. + +For a new 2-opt implementation, test every `i < j` on representative routes +and compare the delta with the change obtained after actually reversing the +segment and recomputing complete route cost. Keep tie ordering exact. Use a +documented floating-point tolerance for cost comparisons; never use a +tolerance to decide which candidate wins the deterministic reduction. + +`docs/2opt-gpu-reduction.md` records the current reduction ABI and optional +Compute Sanitizer commands. A sanitizer pass is evidence for the tested +configuration only. Record tool availability and exact command in the PR. + +## Performance evidence + +Correctness is a prerequisite for timing. A benchmark report belongs under +`docs/benchmarks/` and must include source revision, input hashes, hardware, +driver, compiler, feature flags, timing boundary, sample counts and raw +results. Distinguish full public API latency from kernel-only latency and +separate CPU/GPU move selection from a complete solver comparison. Never +describe a single selected improvement as convergence or a solution-quality +benchmark. See [benchmark records](benchmarks/README.md). diff --git a/docs/user-guide.md b/docs/user-guide.md new file mode 100644 index 0000000..2e88c2c --- /dev/null +++ b/docs/user-guide.md @@ -0,0 +1,103 @@ +# Input model and algorithm semantics + +This guide describes the behavior of the public `vrp-gpu` library at the +current `develop` revision. Read the [crate README](../crates/vrp-gpu/README.md) +first for installation and a compilable CPU example. This is an experimental +API; pin a commit when consuming it from Git. + +## Supported problem + +The implemented objective is total route distance for a **CVRP** +with one depot (node `0`), a finite vehicle fleet, identical vehicle capacity +and nonnegative customer demands. `SolomonInstance` stores ready times, due +times and service times from Solomon input, but the constructive and 2-opt +algorithms do **not** enforce time windows or service schedules. They also do +not model multiple depots, heterogeneous fleets, or inter-route moves. The +parser builds Euclidean edge costs from coordinates; callers can construct a +different finite symmetric matrix manually if it satisfies `validate()`. + +`Route::nodes` contains customer IDs only; travel from and back to depot `0` +is implicit. `Solution::is_feasible` checks the instance, fleet count, capacity, +and exactly one visit to each customer. It does not check time-window +feasibility. An empty route consumes no vehicle. + +## Solomon input and invariants + +Parse text with `input.parse::()`. The parser expects a name, +`VEHICLE` section with count and capacity, and a `CUSTOMER` section with rows +containing at least seven whitespace-separated columns: + +```text +ID X Y DEMAND READY DUE SERVICE +``` + +IDs must start at depot `0` and increase consecutively in row order. The +parser accepts seven or more columns and ignores extras. It is intended for +the Solomon-style layout exercised by this repository, not an arbitrary VRP +file format. Parsing reports `SolomonParseError`; malformed input is not a +normal reason to panic. + +`SolomonInstance` has public fields, so manually built or mutated values must +be checked with `validate()` before use. The check requires: + +- at least one node; positive vehicle count and finite positive capacity; +- all node arrays to have `num_nodes` finite entries; +- nonnegative demands, zero depot demand, and nonnegative service/ready times + with `due >= ready`; +- a `num_nodes × num_nodes` row-major distance matrix with finite, nonnegative, + exactly symmetric entries and a zero diagonal. + +`compute_distance_matrix(xs, ys)` creates that matrix from equal-length +coordinate slices of `f32` values. It does not itself validate coordinates; +call `validate()` on the completed instance. Matrix construction and instance +validation each take O(N²) time and the matrix takes O(N²) memory for N nodes. + +## Constructing and improving a solution + +`nearest_neighbor(&instance)` checks the instance, then repeatedly chooses +the closest unvisited customer that fits the current route's remaining +capacity. Ties retain the first customer ID encountered. It closes a route +when no further customer fits. It panics for invalid instances, oversized +individual demands, or when its greedy choices need more than the available +vehicles. The last condition does not prove mathematical infeasibility. + +The CPU reference offers two levels of 2-opt: + +| API | Effect | +| --- | --- | +| `two_opt_delta(route, instance, i, j)` | Computes the edge-cost change for reversing `i..=j`, with `i < j`; does not mutate. | +| `best_two_opt_move(route, instance)` | Finds the smallest finite negative delta, breaking exact ties by first row-major `(i, j)`; does not mutate. | +| `apply_two_opt(route, i, j)` | Reverses that inclusive segment in place. | +| `two_opt_route(route, instance)` | Repeats best-improvement on one route until no improving move remains. | +| `two_opt(solution, instance)` | Runs that loop independently on each route. | + +The index-based CPU functions expect valid indices and customer IDs. Their +index assertions are debug assertions; callers should not use invalid indices +in release builds. CPU 2-opt assumes the symmetric distance model validated +above; reversing a segment does not change the cost of its internal edges +under this model. Reported improvement sums selected `f32` deltas, which may +differ slightly from a freshly summed full route distance due to rounding. + +## GPU evaluation + +The `gpu` Cargo feature exposes two useful entry points: + +| API | Result | Host transfer | +| --- | --- | --- | +| `gpu::evaluate_two_opt_deltas` | All `n²` route-position deltas, with invalid cells set to `+∞` | Downloads the full delta matrix. | +| `gpu::best_two_opt_move` | One finite negative move or `None` | Reduces on the device; downloads one delta and one index. | + +Both APIs validate the full instance and route IDs before CUDA access. Empty +and singleton routes take a CPU-only fast path. The GPU selector does not +apply the move; it follows the same exact tie rule as the CPU selector. +Approximate floating-point equality is used only in parity tests, not when +ordering candidates. A `None` result means no *finite negative delta was +selected* under the supplied data; it is not a general optimality proof. + +The current GPU host implementation creates a context, loads the embedded PTX, +allocates and transfers data for each nontrivial call. Plan capacity around an +`n²` delta buffer plus temporary reduction buffers. GPU evaluation does not +provide route convergence, batching across routes, or an integrated solver. +The PTX targets `sm_120`; hardware verification has been performed on an RTX +5060 Ti. See [architecture](architecture.md) and the +[reduction contract](2opt-gpu-reduction.md) before changing the kernel.