Advanced opt-in runtime¶
QQA4CO keeps qqa.solve(...) on the portable pure-QQA route unless you
explicitly enable an advanced runtime. None of the features on this page is
required for ordinary CPU, CUDA, or MPS solves.
Alternative binary and simplex geometry¶
The default remains the paper's pure bounded QQA relaxation. Alternative geometry is selected by replacing a problem's relaxation explicitly:
import qqa
# Hard binary objective with a sigmoid surrogate gradient.
problem.relaxation = qqa.StraightThroughBinaryRelaxation(temperature=0.5)
result = qqa.anneal(problem, optimizer="adamw")
# Sparse categorical probabilities (use mapping="entmax15" for entmax).
categorical.relaxation = qqa.SparseCategoricalRelaxation(
mapping="sparsemax",
temperature=2.0,
final_temperature=0.1,
)
result = qqa.anneal(categorical)
# Simplex-native exponentiated-gradient updates.
categorical.relaxation = qqa.MirrorDescentCategoricalRelaxation()
result = qqa.anneal(categorical, optimizer="mirror-descent")
StochasticBinaryRelaxation, EntropicCategoricalRelaxation, softmax/Gumbel,
Sinkhorn, and Gumbel-Sinkhorn follow the same explicit boundary. Temperature
annealing is geometric and endpoint-inclusive. None is selected silently by
the stable pure-QQA route.
Persistent AOT sparse-QUBO cache¶
Use torch.export to capture an autograd-capable graph once and reuse it
across processes and dynamic replica batch sizes:
from qqa.compile import compile_sparse_qubo_aot
compiled = compile_sparse_qubo_aot(
sparse_qubo,
example_values,
cache_dir=".cache/qqa-aot",
)
energies = compiled(values)
print(compiled.cache_hit, compiled.key)
backend="inductor", dynamic_batch=False additionally creates a native
AOTInductor package when the installed PyTorch toolchain provides it. Cache
keys include all coefficients, Torch/Python versions, dtype, device type, and
shape policy, so an incompatible artifact is never reused as if valid.
Fused sparse CUDA and CUDA Graphs¶
Install the optional Triton kernels on a supported Linux/CUDA environment:
Select the fused sparse kernel and static-step CUDA Graph replay explicitly:
import qqa
config = qqa.SolverConfig.for_profile(
"quality",
device="cuda",
sparse_kernel="triton",
cuda_graphs=True,
)
result = qqa.solve(problem, config=config)
sparse_kernel="auto" uses Triton only when it is importable and the values
are CUDA tensors; otherwise it uses the portable PyTorch operation.
cuda_graphs=True requires CUDA and AdamW. It preserves the schedule inputs and
optimizer state while replaying a fixed-shape training step. Keep it disabled
for dynamic-shape custom relaxations.
One QQA island per GPU¶
The distributed engine exchanges a bounded, diverse elite set at coarse round
boundaries. NCCL keeps CUDA migrants device-to-device; Gloo provides the same
contract for CPU testing. Save the following as distributed_solve.py:
import os
import networkx as nx
import torch
import torch.distributed as dist
import qqa
from qqa.engines import anneal_distributed_island
def main() -> None:
dist.init_process_group("nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
device = f"cuda:{local_rank}"
graph = nx.random_regular_graph(d=3, n=500, seed=0)
problem = qqa.MaximumIndependentSet(graph, penalty=2.0)
result = anneal_distributed_island(
problem,
device=device,
sol_size=256,
num_epochs=2000,
rounds=4,
migration_size=16,
seed=local_rank,
)
if dist.get_rank() == 0:
print(result.best_obj)
dist.destroy_process_group()
if __name__ == "__main__":
main()
Launch one process per visible GPU:
The function requires an already-initialised process group. This keeps cluster launch policy outside QQA4CO and makes the same code portable across standard PyTorch launchers.
Bounded advanced local search¶
Sparse binary QUBOs expose explicit, auditable local-search methods:
from qqa.local import iterated_local_search, k_flip_search, tabu_search
incumbent = result.best_sol
tabu = tabu_search(sparse_qubo, incumbent, iterations=500, tenure=11)
kflip = k_flip_search(sparse_qubo, tabu.solution, candidate_width=24)
polished = iterated_local_search(sparse_qubo, kflip.solution, seed=0)
print(polished.objective, polished.moves)
Structure-specific functions include two_opt_tour, three_opt_tour,
maxcut_fm_search, mis_swap_search, kempe_coloring_search, and
walksat_search. They return a solution, objective, move count, and method
name without modifying the caller's input.
Safe QUBO presolve¶
The QUBO presolve helpers never infer a fixing from an unproved heuristic:
from qqa.presolve import (
detect_qubo_symmetries,
dominance_fixings,
exact_probe_persistency,
submodular_roof_duality,
)
fixings = dominance_fixings(sparse_qubo)
symmetry_groups = detect_qubo_symmetries(sparse_qubo)
# Bounded exact persistency; raises instead of silently exceeding the limit.
proof = exact_probe_persistency(sparse_qubo, max_variables=24)
# Available when every pair coefficient is non-positive.
roof = submodular_roof_duality(submodular_qubo)
General ModelIR presolve also performs singleton linear bound propagation and
retains a reversible ledger so returned solutions remain in original space.
Experimental learned helpers¶
Learned helpers are imported from qqa.learned and are never selected by the
default planner:
from qqa.learned import (
DiscreteDiffusionGenerator,
OnlineSolverSelector,
factor_graph_warm_start,
model_features,
)
warm_start = factor_graph_warm_start(model_ir, steps=100, device="cuda")
generator = DiscreteDiffusionGenerator(sparse_qubo)
candidates = generator.generate(128, warm_start=warm_start, seed=0)
selector = OnlineSolverSelector(["qqa", "diffusion"])
features = model_features(model_ir)
backend = selector.select(features)
selector.update(backend, features, reward=1.0)
Treat the selector's reward definition, learned weights, and diffusion budget as application policy. For reproducible comparisons, record them alongside the QQA seed and equal wall-clock budget.