Skip to content

Commit 927c400

Browse files
committed
bench: use solver-reported statistics
1 parent 25aa162 commit 927c400

3 files changed

Lines changed: 54 additions & 123 deletions

File tree

benchmarks/knapsack/KnapsackBenchmark.jl

Lines changed: 29 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -251,79 +251,13 @@ function projection_scaling_rows(; cutoff = 1e-10)
251251
end
252252
end
253253

254-
function penalty_resource_metrics(model; cutoff = 1e-10)
255-
H = TenSolver.tensorize(model.Q, model.l; cutoff, domain = 0:1)
256-
bond = ITensorMPS.maxlinkdim(H)
257-
return (
258-
objective_mpo_bond = bond,
259-
projection_mpo_bond = missing,
260-
effective_hamiltonian_bond = bond,
261-
)
262-
end
263-
264-
function qubo_hamiltonian(Q, l, sites; cutoff)
265-
os = ITensorMPS.OpSum{Float64}()
266-
nvariables = length(sites)
267-
268-
for i in 1:nvariables
269-
linear_coefficient = Q[i, i] + l[i]
270-
if abs(linear_coefficient) > cutoff
271-
os += (linear_coefficient, "D", (domain = 0:1,), i)
272-
end
273-
274-
for j in (i + 1):nvariables
275-
quadratic_coefficient = Q[i, j] + Q[j, i]
276-
if abs(quadratic_coefficient) > cutoff
277-
os += (quadratic_coefficient, "D", (domain = 0:1,), i, "D", (domain = 0:1,), j)
278-
end
279-
end
280-
end
281-
282-
return isempty(os) ? ITensorMPS.MPO(Float64, sites) : ITensorMPS.MPO(Float64, os, sites)
283-
end
284-
285-
function projection_hamiltonian(instance, sites; cutoff)
286-
nitems = length(instance.weights)
287-
H = qubo_hamiltonian(zeros(nitems, nitems), -instance.values, sites; cutoff)
288-
constraint = TenSolver.SumConstraint(
289-
collect(1:nitems),
290-
instance.weights,
291-
instance.capacity;
292-
relation = :(<=),
293-
)
294-
P = TenSolver.projection_mpo(constraint, sites; domain = 0:1)
295-
H_effective = TenSolver.project_hamiltonian(H, P; cutoff)
296-
for i in eachindex(H_effective)
297-
source = (
298-
only(ITensorMPS.siteinds(H_effective, i; plev = 0)),
299-
only(ITensorMPS.siteinds(H_effective, i; plev = 1)),
300-
)
301-
target = (sites[i], sites[i]')
302-
H_effective[i] = ITensors.replaceinds(H_effective[i], source, target)
303-
end
304-
return H_effective
305-
end
306-
307-
function state_variance(H, mps)
308-
expectation = real(ITensors.inner(mps', H, mps))
309-
second_moment = real(ITensors.inner(H, mps, H, mps))
310-
return max(0.0, second_moment - expectation^2)
311-
end
312-
313-
function variance_observer(hamiltonian_builder)
314-
hamiltonian = Ref{Any}()
315-
setup = Ref{Any}()
254+
function variance_observer()
316255
variances = Float64[]
317-
callback = function (mps; kw...)
318-
if !isassigned(hamiltonian)
319-
timed = @timed hamiltonian_builder(ITensorMPS.siteinds(mps))
320-
hamiltonian[] = timed.value
321-
setup[] = (time = timed.time, gctime = timed.gctime, memory = timed.bytes)
322-
end
323-
push!(variances, state_variance(hamiltonian[], mps))
256+
callback = function (_mps; variance, kw...)
257+
push!(variances, variance)
324258
return nothing
325259
end
326-
return variances, setup, callback
260+
return variances, callback
327261
end
328262

329263
function benchmark_repeated(f; samples)
@@ -389,11 +323,16 @@ function runtime_metadata()
389323
end
390324

391325
function solution_stats(solution)
326+
stats = solution.stats
327+
bonds = stats.max_bonds
392328
return (
393-
sweeps = length(solution.energies),
394-
solution_max_bond = isempty(solution.bond_dims) ? 0 : maximum(solution.bond_dims),
395-
solver_elapsed_seconds = isempty(solution.elapsed_times) ? 0.0 :
396-
last(solution.elapsed_times),
329+
sweeps = length(stats.energies),
330+
solution_max_bond = isempty(stats.bond_dims) ? 0 : maximum(stats.bond_dims),
331+
solver_elapsed_seconds = isempty(stats.elapsed_times) ? 0.0 : last(stats.elapsed_times),
332+
initial_state_bond = bonds.initial_state,
333+
objective_mpo_bond = bonds.objective,
334+
projection_mpo_bond = isempty(bonds.projections) ? missing : maximum(bonds.projections),
335+
effective_hamiltonian_bond = bonds.hamiltonian,
397336
)
398337
end
399338

@@ -457,7 +396,10 @@ function solver_options(iterations, cutoff, time_limit, on_iteration)
457396
inidim = 8,
458397
maxdim = [10, 20, 40, 80, 120, 200],
459398
noise = [1e-6, 1e-8, 0.0],
460-
check_variance_every_iteration = iterations + 1,
399+
check_variance_every_iteration = 1,
400+
# Collect variance on every sweep without letting convergence shorten the
401+
# fixed-sweep benchmark workload.
402+
vtol = -Inf,
461403
on_iteration,
462404
callback_every = 1,
463405
verbosity = 0,
@@ -472,8 +414,7 @@ function benchmark_result_row(
472414
reported_objective,
473415
formulation_timed,
474416
case_timed,
475-
solution,
476-
resources;
417+
solution;
477418
variances,
478419
nvariables,
479420
iterations,
@@ -489,16 +430,10 @@ function benchmark_result_row(
489430
feasible = is_capacity_feasible(instance, items)
490431
value = item_value(instance, items)
491432
stats = solution_stats(solution)
492-
observer_setup_seconds = component_median(case_timed.outputs, :observer_setup, :time)
493-
observer_setup_gc_seconds = component_median(case_timed.outputs, :observer_setup, :gctime)
494-
observer_setup_allocated_bytes =
495-
component_median(case_timed.outputs, :observer_setup, :memory)
496433
sampling_seconds = component_median(case_timed.outputs, :sampling, :time)
497434
sampling_gc_seconds = component_median(case_timed.outputs, :sampling, :gctime)
498435
sampling_allocated_bytes = component_median(case_timed.outputs, :sampling, :memory)
499436
solver_call_seconds = max(0.0, case_timed.time - sampling_seconds)
500-
solver_excluding_observer_setup_seconds =
501-
max(0.0, solver_call_seconds - observer_setup_seconds)
502437
metadata = runtime_metadata()
503438

504439
return (;
@@ -531,13 +466,10 @@ function benchmark_result_row(
531466
solve_gc_seconds = case_timed.gctime,
532467
solve_allocated_bytes = case_timed.memory,
533468
solve_allocations = case_timed.allocs,
534-
observer_setup_seconds,
535-
observer_setup_gc_seconds,
536-
observer_setup_allocated_bytes,
537469
sampling_seconds,
538470
sampling_gc_seconds,
539471
sampling_allocated_bytes,
540-
solver_excluding_observer_setup_seconds,
472+
solver_call_seconds,
541473
end_to_end_wall_seconds = formulation_timed.time + case_timed.time,
542474
solver_elapsed_seconds = stats.solver_elapsed_seconds,
543475
sweeps = stats.sweeps,
@@ -546,9 +478,10 @@ function benchmark_result_row(
546478
solution_max_bond = stats.solution_max_bond,
547479
final_variance = isempty(variances) ? missing : last(variances),
548480
truncation_error = missing,
549-
objective_mpo_bond = resources.objective_mpo_bond,
550-
projection_mpo_bond = resources.projection_mpo_bond,
551-
effective_hamiltonian_bond = resources.effective_hamiltonian_bond,
481+
initial_state_bond = stats.initial_state_bond,
482+
objective_mpo_bond = stats.objective_mpo_bond,
483+
projection_mpo_bond = stats.projection_mpo_bond,
484+
effective_hamiltonian_bond = stats.effective_hamiltonian_bond,
552485
)
553486
end
554487

@@ -573,28 +506,22 @@ function projection_row(
573506
end
574507
constraint = formulation_timed.value
575508
case_timed = benchmark_repeated(; samples = timing_samples) do
576-
variances, observer_setup, callback =
577-
variance_observer(sites -> projection_hamiltonian(instance, sites; cutoff),)
509+
variances, callback = variance_observer()
578510
Random.seed!(SOLVER_SEED)
579511
options = solver_options(iterations, cutoff, time_limit, callback)
580512
reported_objective, solution =
581513
TenSolver.maximize(instance.values; constraints = [constraint], options...)
582514
sampling = @timed best_projection_sample(instance, TenSolver.sample(solution, reads))
583-
if !(isassigned(observer_setup))
584-
error("projection variance observer did not run")
585-
end
586515
return (;
587516
reported_objective,
588517
solution,
589518
items = sampling.value,
590519
variances,
591-
observer_setup = observer_setup[],
592520
sampling = (time = sampling.time, gctime = sampling.gctime, memory = sampling.bytes),
593521
)
594522
end
595523
items = assert_stable_items(case_timed.outputs)
596524
output = case_timed.value
597-
resources = projection_resource_metrics(instance; cutoff)
598525
return benchmark_result_row(
599526
instance,
600527
exact,
@@ -603,8 +530,7 @@ function projection_row(
603530
output.reported_objective,
604531
formulation_timed,
605532
case_timed,
606-
output.solution,
607-
resources;
533+
output.solution;
608534
variances = output.variances,
609535
nvariables = nitems,
610536
iterations,
@@ -633,8 +559,7 @@ function penalty_row(
633559
end
634560
model = formulation_timed.value
635561
case_timed = benchmark_repeated(; samples = timing_samples) do
636-
variances, observer_setup, callback =
637-
variance_observer(sites -> qubo_hamiltonian(model.Q, model.l, sites; cutoff),)
562+
variances, callback = variance_observer()
638563
Random.seed!(SOLVER_SEED)
639564
options = solver_options(iterations, cutoff, time_limit, callback)
640565
reported_objective, solution =
@@ -643,22 +568,17 @@ function penalty_row(
643568
assignment = best_penalty_sample(instance, model, TenSolver.sample(solution, reads))
644569
(assignment = assignment, items = item_bits(assignment, model.nitems))
645570
end
646-
if !(isassigned(observer_setup))
647-
error("penalty variance observer did not run")
648-
end
649571
return (;
650572
reported_objective,
651573
solution,
652574
assignment = sampling.value.assignment,
653575
items = sampling.value.items,
654576
variances,
655-
observer_setup = observer_setup[],
656577
sampling = (time = sampling.time, gctime = sampling.gctime, memory = sampling.bytes),
657578
)
658579
end
659580
items = assert_stable_items(case_timed.outputs)
660581
output = case_timed.value
661-
resources = penalty_resource_metrics(model; cutoff)
662582
return benchmark_result_row(
663583
instance,
664584
exact,
@@ -667,8 +587,7 @@ function penalty_row(
667587
output.reported_objective,
668588
formulation_timed,
669589
case_timed,
670-
output.solution,
671-
resources;
590+
output.solution;
672591
variances = output.variances,
673592
nvariables = length(output.assignment),
674593
iterations,
@@ -707,7 +626,8 @@ Run the hard-projection solve and a penalty-QUBO sensitivity sweep for every
707626
instance. Returned rows report the original knapsack objective and feasibility,
708627
not just each solver's encoded objective.
709628
710-
Final-state variance is calculated from the MPS supplied to `on_iteration`.
629+
Final-state variance and operator bond dimensions come from solver-reported
630+
statistics.
711631
Truncation error remains unavailable because the callback runs after discarded
712632
singular values have been removed.
713633

benchmarks/knapsack/README.md

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,11 @@ each formulation to reduce one-sided Julia compilation bias in the timing.
6666
Reported wall time, GC time, allocations, and allocated bytes come from
6767
`BenchmarkTools.jl`. Each expensive solver case uses one evaluation per sample
6868
and, by default, the median of three fixed-seed samples. Formulation construction
69-
is measured separately from the public solver call. Callback-Hamiltonian setup
70-
and final sampling are also reported separately, while `end_to_end_wall_seconds`
71-
includes formulation construction and the complete measured solver/sample case.
72-
`solver_excluding_observer_setup_seconds` subtracts only the independently
73-
measured callback-Hamiltonian construction; it still includes the public solver's
74-
own tensorization/projection work and per-sweep variance calculations.
69+
is measured separately from the public solver call. Final sampling is also
70+
reported separately, while `end_to_end_wall_seconds` includes formulation
71+
construction and the complete measured solver/sample case.
72+
`solver_call_seconds` subtracts sampling from that measured case and includes
73+
TenSolver's tensorization, projection, and per-sweep variance calculations.
7574

7675
For each instance the runner executes the projection formulation once and the
7776
penalty formulation at factors `0.001`, `0.01`, `0.1`, and `1.1` times the sum
@@ -101,10 +100,10 @@ Rows report:
101100

102101
- original knapsack value, feasibility, and gap from the exact feasible optimum;
103102
- penalty factor, coefficient, and encoded objective for penalty-QUBO rows;
104-
- formulation, solve, callback-Hamiltonian setup, sampling, and end-to-end wall
105-
times, with timing sample count and per-solve limit;
103+
- formulation, solver-call, sampling, measured-case, and end-to-end wall times,
104+
with timing sample count and per-solve limit;
106105
- solver-reported elapsed time, requested/actual sweep count, time-limit status,
107-
maximum solution MPS bond dimension, and final-state variance;
106+
initial/maximum solution MPS bond dimensions, and final-state variance;
108107
- GC time and allocations for the formulation and measured solve case;
109108
- objective, projection, and effective projected-Hamiltonian MPO bond dimensions.
110109

@@ -115,13 +114,17 @@ gap, variance, and whether the time limit was reached; equal sweep counts do not
115114
imply equal convergence quality.
116115

117116
The projection and effective-Hamiltonian bonds are separate because the latter
118-
drives constrained DMRG cost. Final variance is calculated independently from
119-
the per-sweep MPS supplied by `on_iteration`. Truncation error remains empty:
120-
the callback runs after the DMRG sweep has discarded singular values, so that
121-
error cannot be reconstructed from the retained MPS.
117+
drives constrained DMRG cost. The solver reports these bonds through
118+
`solution.stats.max_bonds` and supplies its calculated variance to
119+
`on_iteration`, so the benchmark does not reconstruct the Hamiltonian or repeat
120+
the variance calculation. Truncation error remains empty: the callback runs
121+
after the DMRG sweep has discarded singular values, so that error cannot be
122+
reconstructed from the retained MPS.
122123

123124
## Recorded results
124125

125126
The repository includes a pinned complete run with its raw CSV, exact command,
126127
machine and package metadata, quality-aware interpretation, and limitations in
127-
[`results/README.md`](results/README.md).
128+
[`results/README.md`](results/README.md). That historical run predates
129+
solver-reported statistics and therefore retains the old observer-setup timing
130+
columns; new output uses the simpler schema documented above.

benchmarks/knapsack/results/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ julia +1.12 --project=benchmarks benchmarks/knapsack/run.jl \
2626

2727
All 25 rows completed all six requested sweeps without reaching the time limit.
2828

29+
This pinned dataset predates TenSolver's solver-reported variance and operator
30+
bond statistics. Its `observer_setup_*` and
31+
`solver_excluding_observer_setup_seconds` columns describe the historical
32+
harness that produced it. Current benchmark runs no longer reconstruct the
33+
Hamiltonian or recalculate variance in the callback, and instead report
34+
`solver_call_seconds` plus `initial_state_bond`; timings across the two schemas
35+
should not be compared as if their instrumentation were identical.
36+
2937
## Results
3038

3139
"Best feasible penalty" selects the penalty row with the highest original

0 commit comments

Comments
 (0)