Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
274727d
Add a binary/int8 fast path to the CPU feasibility jump
aliceb-nv Aug 6, 2026
8c33ca4
Take the tabu test out of the binary fast path's global argmax
aliceb-nv Aug 6, 2026
fb16f72
perf(cpufj-bin): drop the per-iteration bitmap clear and hoist apply_…
aliceb-nv Aug 6, 2026
7c95a09
perf(cpufj-bin): vectorize apply_move's row walk
aliceb-nv Aug 6, 2026
e57dd60
docs: record the Highway third-party licence
aliceb-nv Aug 7, 2026
d8a8224
bit of cleanup
aliceb-nv Aug 7, 2026
3b1a7bb
cleanup
aliceb-nv Aug 7, 2026
ea825f9
perf: fold the patch width choice into the Highway target
aliceb-nv Aug 7, 2026
e63f294
perf: normalize every row to a'x <= b and drop the per-row sign
aliceb-nv Aug 7, 2026
fa4d037
refactor: replace the row record with a plain weight array
aliceb-nv Aug 7, 2026
c07c578
fix: derive the argmax tile from the L1 data cache size
aliceb-nv Aug 7, 2026
667f87f
debug: add CUOPT_NO_BINFJ to force the general path
aliceb-nv Aug 7, 2026
8c5a976
fix: widen the packed staged score to int64
aliceb-nv Aug 7, 2026
7af8176
add row integralize
aliceb-nv Aug 18, 2026
237d04e
fix solve_CPUFJ link
aliceb-nv Aug 19, 2026
6d32162
latency tweaks
aliceb-nv Aug 19, 2026
fead636
more logs
aliceb-nv Aug 19, 2026
4f3b981
ai review
aliceb-nv Aug 19, 2026
cc05b56
fix build
aliceb-nv Aug 19, 2026
33daa1d
some optimizing
aliceb-nv Aug 20, 2026
7dfaeb5
some bug fixes regarding lift moves
aliceb-nv Aug 20, 2026
03be83c
hiverge inspired improvements
aliceb-nv Aug 20, 2026
a3e4387
ddfw improvements
aliceb-nv Aug 20, 2026
35307fb
changes for the hiverge harness
aliceb-nv Aug 20, 2026
b325b87
replace operator overloaded arithmetic with explicit highway function…
aliceb-nv Aug 20, 2026
eca99b3
save user_callbacks in run_mip
aliceb-nv Aug 20, 2026
ed1482c
let B&B publih solutions via the user callbacks as well to reduce lat…
aliceb-nv Aug 20, 2026
cd3de18
publish cpufj scratch solutions early as well
aliceb-nv Aug 20, 2026
5fdc261
absorb cuda driver startup in run_mip.cpp
aliceb-nv Aug 20, 2026
e93f63b
fix multigpu runs
aliceb-nv Aug 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
215 changes: 215 additions & 0 deletions benchmarks/linear_programming/cuopt/run_cpufj.cu
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/* clang-format off */
/*
* SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
/* clang-format on */

#include <mip_heuristics/feasibility_jump/fj_cpu.cuh>
#include <mip_heuristics/problem/problem.cuh>
#include <mip_heuristics/solution/solution.cuh>
#include <mip_heuristics/utils.cuh>

#include <cuopt/mathematical_optimization/io/parser.hpp>
#include <cuopt/mathematical_optimization/solve.hpp>
#include <utilities/logger.hpp>

#include <raft/core/handle.hpp>

#include <pthread.h>
#include <sched.h>

#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <limits>
#include <string>
#include <vector>

namespace {

using i_t = int;
using f_t = double;
namespace mip = cuopt::mathematical_optimization::mip;

using clk = std::chrono::high_resolution_clock;
double since(clk::time_point t0)
{
return std::chrono::duration_cast<std::chrono::duration<double>>(clk::now() - t0).count();
}

struct climber_result_t {
bool crossed{false};
double t_first{-1.0};
f_t best_objective{std::numeric_limits<f_t>::infinity()};
i_t iterations{0};
double seconds{0.0};
};

void pin_to_core(int core)
{
cpu_set_t set;
CPU_ZERO(&set);
CPU_SET(core, &set);
pthread_setaffinity_np(pthread_self(), sizeof(set), &set);
}

// The CPUs this process is actually permitted to run on. A cgroup mask can be non-contiguous, so
// indexing hardware_concurrency() directly would collide several climbers onto one core.
std::vector<int> allowed_cpus()
{
std::vector<int> allowed;
cpu_set_t set;
CPU_ZERO(&set);
if (sched_getaffinity(0, sizeof(set), &set) == 0) {
for (int cpu = 0; cpu < CPU_SETSIZE; ++cpu) {
if (CPU_ISSET(cpu, &set)) allowed.push_back(cpu);
}
}
if (allowed.empty()) allowed.push_back(0);
return allowed;
}

void run_climber(mip::fj_cpu_climber_t<i_t, f_t>* climber,
f_t time_limit,
int core,
climber_result_t& result)
{
pin_to_core(core);
const auto t0 = clk::now();

climber->improvement_callback = [&result, t0](f_t objective, const std::vector<f_t>&, double) {
if (!result.crossed) {
result.crossed = true;
result.t_first = since(t0);
}
result.best_objective = objective;
};

mip::cpufj_solve(climber, time_limit);

result.seconds = since(t0);
result.iterations = climber->iterations;
}

} // namespace

int main(int argc, char** argv)
{
if (argc < 2) {
std::fprintf(stderr, "usage: %s <instance.mps> [time_limit_s=60] [climbers=16] [seed=12345]\n",
argv[0]);
return 2;
}
const std::string path = argv[1];
const f_t time_limit = argc > 2 ? std::atof(argv[2]) : 60.0;
const int n_climbers = argc > 3 ? std::atoi(argv[3]) : 16;
const unsigned base_seed = argc > 4 ? (unsigned)std::atoll(argv[4]) : 12345u;

// Console sink so the engine's end-of-solve incumbent audit is visible, as solve_MIP does it.
cuopt::init_logger_t log_guard("", true);

raft::handle_t handle;

const auto mps_data_model = cuopt::mathematical_optimization::io::read_mps<i_t, f_t>(path, false);
const auto op_problem =
cuopt::mathematical_optimization::mps_data_model_to_optimization_problem<i_t, f_t>(
&handle, mps_data_model);
mip::problem_t<i_t, f_t> problem(op_problem);

// Anonymise the instance before anything under evolution can see it.
//
// problem_t exposes var_names, row_names and objective_name as public members, and
// the FJ code receives problem_t&. For a fixed benchmark set those strings are an
// exact fingerprint -- row_names[0] alone identifies most MIPLIB instances -- so a
// candidate could branch on identity and return a memorised objective. Reading the
// MODEL is intended and useful: coefficients, bounds, variable types, sparsity and
// row structure are all untouched here, so recognising set-packing rows, knapsack
// substructure or GUB constraints still works exactly as before. Only the labels go.
//
// Each string is cleared in place rather than the vectors being emptied, so size()
// and indexing stay valid and any code that walks names by variable index still
// works -- it just gets empty strings.
//
// This file is outside target_code and is sha256-gated by evaluate.py's FROZEN_FILES,
// so a candidate cannot restore the names. Do not move this below the solve.
for (auto& name : problem.var_names) name.clear();
for (auto& name : problem.row_names) name.clear();
problem.objective_name.clear();

std::printf("instance: %s n_vars=%d n_cstrs=%d nnz=%d\n",
path.c_str(),
problem.n_variables,
problem.n_constraints,
problem.nnz);

// FROZEN -- defines t=0 for the benchmark. Everything above it (the MPS parse,
// problem construction under problem/, and the name anonymisation) is outside
// target_code; everything below it is editable. A marker any later would leave
// editable code ahead of the clock, which is somewhere to do unmeasured work; any
// earlier would charge the budget for a parse and a CUDA context no candidate can
// influence.
CUOPT_LOG_INFO("CPUFJ solve window start");

// Shared by every climber. Built by build_start_assignment, which is editable --
// this driver is not.
mip::solution_t<i_t, f_t> solution(problem);
mip::build_start_assignment<i_t, f_t>(problem, solution, &handle);

std::vector<std::atomic<bool>> preemption_flags(n_climbers);
std::vector<std::unique_ptr<mip::fj_cpu_climber_t<i_t, f_t>>> climbers(n_climbers);
// Composition and per-climber parameters come from build_climber_portfolio, which
// is editable. The log prefix is assigned here and not there, so every climber
// stays identifiable in the log whatever the portfolio does.
mip::build_climber_portfolio<i_t, f_t>(problem, solution, preemption_flags, climbers, base_seed);
for (int k = 0; k < n_climbers; ++k) {
climbers[k]->log_prefix = "[climber " + std::to_string(k) + "] ";
}

const std::vector<int> cpus = allowed_cpus();
std::printf("running %d climbers x %.0fs, base seed %u, %zu allowed CPUs (%d..%d)\n",
n_climbers, (double)time_limit, base_seed, cpus.size(), cpus.front(), cpus.back());

std::vector<climber_result_t> results(n_climbers);
std::vector<std::thread> threads;
threads.reserve(n_climbers);
const auto wall0 = clk::now();
for (int k = 0; k < n_climbers; ++k) {
threads.emplace_back(
run_climber, climbers[k].get(), time_limit, cpus[k % cpus.size()], std::ref(results[k]));
}
for (auto& t : threads) {
t.join();
}
const double wall = since(wall0);

int crossed = 0;
double sum_iters = 0;
f_t best_overall = std::numeric_limits<f_t>::infinity();
std::printf("\n climber | crossed | t_first(s) | obj | iters | iters/s\n");
std::printf("---------+---------+------------+--------------+----------+---------\n");
for (int k = 0; k < n_climbers; ++k) {
const auto& r = results[k];
sum_iters += r.iterations;
if (r.crossed) {
++crossed;
best_overall = std::min(best_overall, r.best_objective);
}
std::printf(" %7d | %7s | %10s | %12.6g | %8d | %8.0f\n",
k,
r.crossed ? "YES" : "no",
r.crossed ? std::to_string(r.t_first).c_str() : "-",
r.crossed ? (double)r.best_objective : 0.0,
r.iterations,
r.seconds > 0 ? r.iterations / r.seconds : 0.0);
}
std::printf("\nSUMMARY: %d/%d crossed (%.0f%%) wall=%.1fs total_iters=%.0f agg_iters/s=%.0f\n",
crossed,
n_climbers,
100.0 * crossed / n_climbers,
wall,
sum_iters,
wall > 0 ? sum_iters / wall : 0.0);
if (crossed > 0) { std::printf("BEST OBJECTIVE: %.10g\n", (double)best_overall); }
return 0;
}
58 changes: 58 additions & 0 deletions benchmarks/linear_programming/cuopt/run_mip.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <cuopt/mathematical_optimization/mip/solver_solution.hpp>
#include <cuopt/mathematical_optimization/optimization_problem_interface.hpp>
#include <cuopt/mathematical_optimization/solve.hpp>
#include <cuopt/mathematical_optimization/utilities/internals.hpp>
#include <utilities/logger.hpp>

#include <raft/core/handle.hpp>
Expand Down Expand Up @@ -136,6 +137,52 @@ std::vector<std::vector<double>> read_solution_from_dir(const std::string file_p
return initial_solutions;
}

struct incumbent_record_t {
double objective;
double work_timestamp;
double wall_time;
};

class incumbent_tracker_t : public cuopt::internals::get_solution_callback_t {
public:
explicit incumbent_tracker_t(std::chrono::high_resolution_clock::time_point start_time)
: start_time_(start_time)
{
}

void get_solution(void* /*data*/,
void* cost,
void* /*solution_bound*/,
void* /*user_data*/) override
{
const auto now = std::chrono::high_resolution_clock::now();
records_.push_back({*static_cast<double*>(cost),
0.0,
std::chrono::duration<double>(now - start_time_).count()});
}

void write_csv(const std::string& path) const
{
std::ofstream file(path);
if (!file.is_open()) {
std::cerr << "Error opening incumbent file " << path << std::endl;
return;
}
file << "index,objective,work_timestamp,wall_time_s\n";
for (size_t i = 0; i < records_.size(); ++i) {
file << i << "," << std::setprecision(15) << records_[i].objective << ","
<< records_[i].work_timestamp << "," << std::setprecision(6) << records_[i].wall_time
<< "\n";
}
}

size_t size() const { return records_.size(); }

private:
std::chrono::high_resolution_clock::time_point start_time_;
std::vector<incumbent_record_t> records_;
};

int run_single_file(std::string file_path,
int device,
int batch_id,
Expand All @@ -151,6 +198,8 @@ int run_single_file(std::string file_path,
double work_limit,
bool deterministic)
{
(void)cudaFree(0);

const raft::handle_t handle_{};
cuopt::mathematical_optimization::mip_solver_settings_t<int, double> settings;
std::string base_filename = file_path.substr(file_path.find_last_of("/\\") + 1);
Expand Down Expand Up @@ -218,6 +267,8 @@ int run_single_file(std::string file_path,
cuopt::mathematical_optimization::benchmark_info_t benchmark_info;
settings.benchmark_info_ptr = &benchmark_info;
auto start_run_solver = std::chrono::high_resolution_clock::now();
incumbent_tracker_t incumbent_tracker(start_run_solver);
settings.set_mip_callback(&incumbent_tracker);
auto solution = cuopt::mathematical_optimization::solve_mip(&handle_, mps_data_model, settings);
CUOPT_LOG_INFO(
"first obj: %f last improvement of best feasible: %f last improvement after recombination: %f",
Expand Down Expand Up @@ -291,6 +342,13 @@ int run_single_file(std::string file_path,
<< "\n";
write_to_output_file(out_dir, base_filename, device, n_gpus, batch_id, ss.str());
CUOPT_LOG_INFO("Results written to the file %s", base_filename.c_str());
if (out_dir != "") {
std::string csv_path =
out_dir + "/" + base_filename.substr(0, base_filename.find(".mps")) + "_incumbents.csv";
incumbent_tracker.write_csv(csv_path);
CUOPT_LOG_INFO(
"Incumbent trace (%zu entries) written to %s", incumbent_tracker.size(), csv_path.c_str());
}
return sol_found;
}

Expand Down
Loading