Skip to content

Commit de40ab2

Browse files
committed
feat(bench): PPR push-cap truncation flag + mean alongside min
1 parent 3ecb8dc commit de40ab2

7 files changed

Lines changed: 137 additions & 34 deletions

File tree

benchmarks/adapters/calibrate.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@ def score(self) -> float:
5555
return 0.0
5656
return min(agg.get("file_recall", 0.0) for agg in self.per_benchmark.values())
5757

58+
@property
59+
def score_mean(self) -> float:
60+
"""Macro-mean recall across benchmarks. Reported alongside
61+
`score` (= min) so the paper can show that the selected cell
62+
wins by both — defending against a "cherry-picked min" critique
63+
when the surface is flat."""
64+
if not self.per_benchmark:
65+
return 0.0
66+
recalls = [agg.get("file_recall", 0.0) for agg in self.per_benchmark.values()]
67+
return sum(recalls) / len(recalls) if recalls else 0.0
68+
5869

5970
TrialCallback = Callable[[int, int, "TrialResult"], None]
6071

benchmarks/calibrate.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def _on_trial(idx: int, total: int, trial) -> None:
127127
print(
128128
f"[{idx + 1}/{total}] τ={trial.params.tau:.4f} "
129129
f"cbf={trial.params.core_budget_fraction:.4f} → "
130-
f"min(per_benchmark file_recall) = {trial.score:.4f}"
130+
f"min={trial.score:.4f} mean={trial.score_mean:.4f}"
131131
)
132132

133133
trials = evaluate_grid_cached(
@@ -153,6 +153,7 @@ def _on_trial(idx: int, total: int, trial) -> None:
153153
{
154154
"params": asdict(t.params),
155155
"score": t.score,
156+
"score_mean": t.score_mean,
156157
"per_benchmark": t.per_benchmark,
157158
}
158159
for t in trials
@@ -161,11 +162,15 @@ def _on_trial(idx: int, total: int, trial) -> None:
161162
(args.out / "grid_results.json").write_text(json.dumps(payload, indent=2, default=str))
162163

163164
top = top_k_trials(trials, k=args.top_k)
164-
top_payload = {"top_k": args.top_k, "candidates": [asdict(t.params) for t in top]}
165+
top_payload = {
166+
"top_k": args.top_k,
167+
"candidates": [asdict(t.params) for t in top],
168+
"candidate_scores": [{"params": asdict(t.params), "score_min": t.score, "score_mean": t.score_mean} for t in top],
169+
}
165170
(args.out / "top_candidates.json").write_text(json.dumps(top_payload, indent=2, default=str))
166-
print(f"\nTop {args.top_k} candidates by min(per_benchmark file_recall):")
171+
print(f"\nTop {args.top_k} candidates by min(per_benchmark file_recall) [mean shown for cherry-pick check]:")
167172
for t in top:
168-
print(f" τ={t.params.tau} cbf={t.params.core_budget_fraction} score={t.score:.4f}")
173+
print(f" τ={t.params.tau} cbf={t.params.core_budget_fraction}" f" min={t.score:.4f} mean={t.score_mean:.4f}")
169174
return 0
170175

171176

diffctx/src/pipeline.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,9 @@ pub fn select_with_params(
432432
edges_dropped_by_cap: cap_stats.edges_dropped_by_cap,
433433
nodes_capped: cap_stats.nodes_capped,
434434
max_out_edges_per_node: cap_stats.max_out_edges_per_node,
435+
ppr_truncated: state.scoring_result.ppr_truncated,
436+
ppr_forward_pushes: state.scoring_result.ppr_forward_pushes,
437+
ppr_backward_pushes: state.scoring_result.ppr_backward_pushes,
435438
});
436439
output
437440
}
@@ -496,6 +499,9 @@ fn empty_scored_state(root_dir: PathBuf) -> ScoredState {
496499
rel_scores: FxHashMap::default(),
497500
filtered_fragments: Vec::new(),
498501
graph: crate::graph::Graph::new(),
502+
ppr_truncated: false,
503+
ppr_forward_pushes: 0,
504+
ppr_backward_pushes: 0,
499505
},
500506
needs: Vec::new(),
501507
changed_files: Vec::new(),

diffctx/src/ppr.rs

Lines changed: 74 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,32 @@ fn init_seed_residuals(
4747
residual
4848
}
4949

50+
/// Result of one PPR push pass. `truncated` flags whether the
51+
/// `max_pushes` budget cut the iteration short — when true, the
52+
/// returned estimate is biased toward seeds and the absolute scores
53+
/// are not comparable to a converged run on the same graph. Surfaced
54+
/// to Python via `LatencyBreakdown.ppr_truncated` so calibration /
55+
/// final-eval rows can be filtered or flagged for the paper.
56+
struct PprPushResult {
57+
estimate: Vec<f64>,
58+
pushes: usize,
59+
truncated: bool,
60+
}
61+
5062
fn ppr_push_csr(
5163
csr: &CsrGraph,
5264
seeds: &FxHashSet<FragmentId>,
5365
alpha: f64,
5466
tol: f64,
5567
seed_weights: Option<&FxHashMap<FragmentId, f64>>,
56-
) -> Vec<f64> {
68+
) -> PprPushResult {
5769
let n = csr.n;
5870
if n == 0 {
59-
return Vec::new();
71+
return PprPushResult {
72+
estimate: Vec::new(),
73+
pushes: 0,
74+
truncated: false,
75+
};
6076
}
6177

6278
let restart = 1.0 - alpha;
@@ -74,9 +90,11 @@ fn ppr_push_csr(
7490

7591
let max_pushes = (n * PPR.push_scale_factor).min(PPR.max_pushes_cap);
7692
let mut pushes: usize = 0;
93+
let mut truncated = false;
7794

7895
while let Some(u) = queue.pop_front() {
7996
if pushes >= max_pushes {
97+
truncated = true;
8098
break;
8199
}
82100
let ui = u as usize;
@@ -114,7 +132,26 @@ fn ppr_push_csr(
114132
pushes += 1;
115133
}
116134

117-
estimate
135+
PprPushResult {
136+
estimate,
137+
pushes,
138+
truncated,
139+
}
140+
}
141+
142+
/// Public PPR result. `truncated` is logical-OR of forward + backward
143+
/// push truncation flags. When true, downstream renormalization
144+
/// (sum-to-1 across nodes) hides the fact that the iteration was cut
145+
/// short by `max_pushes_cap`; the absolute relevance scores are
146+
/// biased toward seeds and not directly comparable across instances.
147+
/// We surface this to Python so calibration / final-eval rows can be
148+
/// filtered in post-analysis (PolyBench / Multi-SWE-bench instances
149+
/// with >20k fragments are the primary suspects).
150+
pub struct PprResult {
151+
pub scores: FxHashMap<FragmentId, f64>,
152+
pub truncated: bool,
153+
pub forward_pushes: usize,
154+
pub backward_pushes: usize,
118155
}
119156

120157
pub fn personalized_pagerank(
@@ -124,26 +161,28 @@ pub fn personalized_pagerank(
124161
tol: f64,
125162
forward_blend: f64,
126163
seed_weights: Option<&FxHashMap<FragmentId, f64>>,
127-
) -> FxHashMap<FragmentId, f64> {
128-
if graph.node_count() == 0 {
129-
return FxHashMap::default();
130-
}
131-
132-
if seeds.is_empty() {
133-
return FxHashMap::default();
164+
) -> PprResult {
165+
if graph.node_count() == 0 || seeds.is_empty() {
166+
return PprResult {
167+
scores: FxHashMap::default(),
168+
truncated: false,
169+
forward_pushes: 0,
170+
backward_pushes: 0,
171+
};
134172
}
135173

136174
let (fwd_csr, rev_csr) = graph.to_csr();
137175

138-
let (forward_est, backward_est) = rayon::join(
176+
let (forward, backward) = rayon::join(
139177
|| ppr_push_csr(fwd_csr, seeds, alpha, tol, seed_weights),
140178
|| ppr_push_csr(rev_csr, seeds, alpha, tol, seed_weights),
141179
);
142180

143181
let n = fwd_csr.n;
144182
let mut combined = vec![0.0f64; n];
145183
for i in 0..n {
146-
combined[i] = forward_blend * forward_est[i] + (1.0 - forward_blend) * backward_est[i];
184+
combined[i] =
185+
forward_blend * forward.estimate[i] + (1.0 - forward_blend) * backward.estimate[i];
147186
}
148187

149188
let total: f64 = combined.iter().sum();
@@ -154,14 +193,19 @@ pub fn personalized_pagerank(
154193
}
155194

156195
let idx_to_node = &fwd_csr.idx_to_node;
157-
let mut result: FxHashMap<FragmentId, f64> = FxHashMap::default();
196+
let mut scores: FxHashMap<FragmentId, f64> = FxHashMap::default();
158197
for i in 0..n {
159198
if combined[i] > 0.0 {
160-
result.insert(idx_to_node[i].clone(), combined[i]);
199+
scores.insert(idx_to_node[i].clone(), combined[i]);
161200
}
162201
}
163202

164-
result
203+
PprResult {
204+
scores,
205+
truncated: forward.truncated || backward.truncated,
206+
forward_pushes: forward.pushes,
207+
backward_pushes: backward.pushes,
208+
}
165209
}
166210

167211
#[cfg(test)]
@@ -177,7 +221,7 @@ mod tests {
177221
fn ppr_empty_graph() {
178222
let mut g = Graph::new();
179223
let seeds = FxHashSet::default();
180-
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-4, 0.4, None);
224+
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-4, 0.4, None).scores;
181225
assert!(result.is_empty());
182226
}
183227

@@ -189,7 +233,7 @@ mod tests {
189233

190234
let mut seeds = FxHashSet::default();
191235
seeds.insert(a.clone());
192-
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-4, 0.4, None);
236+
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-4, 0.4, None).scores;
193237
assert!((result[&a] - 1.0).abs() < 1e-6);
194238
}
195239

@@ -207,7 +251,7 @@ mod tests {
207251

208252
let mut seeds = FxHashSet::default();
209253
seeds.insert(a.clone());
210-
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-6, 0.4, None);
254+
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-6, 0.4, None).scores;
211255

212256
assert!(result[&a] > result[&b]);
213257
assert!(result[&b] > result[&c]);
@@ -228,7 +272,7 @@ mod tests {
228272

229273
let mut seeds = FxHashSet::default();
230274
seeds.insert(a.clone());
231-
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-6, 0.4, None);
275+
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-6, 0.4, None).scores;
232276

233277
let total: f64 = result.values().sum();
234278
assert!((total - 1.0).abs() < 1e-6);
@@ -251,7 +295,7 @@ mod tests {
251295
sw.insert(a.clone(), 0.9);
252296
sw.insert(b.clone(), 0.1);
253297

254-
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-6, 0.4, Some(&sw));
298+
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-6, 0.4, Some(&sw)).scores;
255299
assert!(result[&a] > result[&b]);
256300
}
257301

@@ -274,8 +318,8 @@ mod tests {
274318
let (mut g2, _) = build_star_graph();
275319
let seeds: FxHashSet<FragmentId> = std::iter::once(center).collect();
276320

277-
let r1 = personalized_pagerank(&mut g1, &seeds, 0.6, 1e-6, 0.4, None);
278-
let r2 = personalized_pagerank(&mut g2, &seeds, 0.6, 1e-6, 0.4, None);
321+
let r1 = personalized_pagerank(&mut g1, &seeds, 0.6, 1e-6, 0.4, None).scores;
322+
let r2 = personalized_pagerank(&mut g2, &seeds, 0.6, 1e-6, 0.4, None).scores;
279323

280324
assert_eq!(r1.len(), r2.len());
281325
for (id, v1) in &r1 {
@@ -290,8 +334,8 @@ mod tests {
290334
let (mut g_tight, _) = build_star_graph();
291335
let seeds: FxHashSet<FragmentId> = std::iter::once(center).collect();
292336

293-
let loose = personalized_pagerank(&mut g_loose, &seeds, 0.6, 1e-2, 0.4, None);
294-
let tight = personalized_pagerank(&mut g_tight, &seeds, 0.6, 1e-6, 0.4, None);
337+
let loose = personalized_pagerank(&mut g_loose, &seeds, 0.6, 1e-2, 0.4, None).scores;
338+
let tight = personalized_pagerank(&mut g_tight, &seeds, 0.6, 1e-6, 0.4, None).scores;
295339

296340
let max_diff = loose
297341
.iter()
@@ -307,7 +351,7 @@ mod tests {
307351
fn ppr_symmetric_star_assigns_equal_mass_to_leaves() {
308352
let (mut g, center) = build_star_graph();
309353
let seeds: FxHashSet<FragmentId> = std::iter::once(center.clone()).collect();
310-
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-8, 0.5, None);
354+
let result = personalized_pagerank(&mut g, &seeds, 0.6, 1e-8, 0.5, None).scores;
311355

312356
let leaf_scores: Vec<f64> = (0..5)
313357
.map(|i| result[&fid(&format!("leaf_{i}.rs"), 1, 10)])
@@ -384,8 +428,9 @@ mod tests {
384428
let tol = 1e-8;
385429
let blend = 1.0;
386430

387-
let r_naive = personalized_pagerank(&mut naive, &seeds, alpha, tol, blend, None);
388-
let r_suppressed = personalized_pagerank(&mut suppressed, &seeds, alpha, tol, blend, None);
431+
let r_naive = personalized_pagerank(&mut naive, &seeds, alpha, tol, blend, None).scores;
432+
let r_suppressed =
433+
personalized_pagerank(&mut suppressed, &seeds, alpha, tol, blend, None).scores;
389434

390435
let hub_naive = r_naive.get(&hub).copied().unwrap_or(0.0);
391436
let hub_suppressed = r_suppressed.get(&hub).copied().unwrap_or(0.0);
@@ -449,7 +494,7 @@ mod tests {
449494
}
450495

451496
let seeds: FxHashSet<FragmentId> = nodes.iter().take(2).cloned().collect();
452-
let ppr_scores = personalized_pagerank(&mut g, &seeds, 0.6, 1e-8, 0.5, None);
497+
let ppr_scores = personalized_pagerank(&mut g, &seeds, 0.6, 1e-8, 0.5, None).scores;
453498
let ego_scores = g.ego_graph(&seeds, 3);
454499

455500
let common: Vec<FragmentId> = nodes
@@ -535,7 +580,7 @@ mod tests {
535580
g.add_edge(leaf.clone(), center.clone(), 1.0);
536581
}
537582
let seeds: FxHashSet<FragmentId> = std::iter::once(center.clone()).collect();
538-
let result = personalized_pagerank(&mut g, &seeds, alpha, 1e-10, 0.5, None);
583+
let result = personalized_pagerank(&mut g, &seeds, alpha, 1e-10, 0.5, None).scores;
539584

540585
let expected_center = 1.0 / (1.0 + alpha);
541586
let expected_leaf = alpha / (n_leaves as f64 * (1.0 + alpha));

diffctx/src/pybridge.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,9 @@ fn build_diff_context<'py>(
306306
latency.set_item("edges_dropped_by_cap", lb.edges_dropped_by_cap)?;
307307
latency.set_item("nodes_capped", lb.nodes_capped)?;
308308
latency.set_item("max_out_edges_per_node", lb.max_out_edges_per_node)?;
309+
latency.set_item("ppr_truncated", lb.ppr_truncated)?;
310+
latency.set_item("ppr_forward_pushes", lb.ppr_forward_pushes)?;
311+
latency.set_item("ppr_backward_pushes", lb.ppr_backward_pushes)?;
309312
} else {
310313
latency.set_item("total_ms", (total_ms * 10.0).round() / 10.0)?;
311314
}
@@ -424,6 +427,9 @@ fn diff_context_output_to_dict<'py>(
424427
latency.set_item("edges_dropped_by_cap", lb.edges_dropped_by_cap)?;
425428
latency.set_item("nodes_capped", lb.nodes_capped)?;
426429
latency.set_item("max_out_edges_per_node", lb.max_out_edges_per_node)?;
430+
latency.set_item("ppr_truncated", lb.ppr_truncated)?;
431+
latency.set_item("ppr_forward_pushes", lb.ppr_forward_pushes)?;
432+
latency.set_item("ppr_backward_pushes", lb.ppr_backward_pushes)?;
427433
}
428434
dict.set_item("latency", latency)?;
429435
Ok(dict)

diffctx/src/render.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ pub struct LatencyBreakdown {
5858
pub nodes_capped: usize,
5959
/// The K value applied for the per-source cap.
6060
pub max_out_edges_per_node: usize,
61+
/// PPR push iteration was truncated by `max_pushes_cap` before
62+
/// convergence. When true, `rel_scores` are biased toward seeds
63+
/// and absolute file_recall on this instance should be flagged
64+
/// in post-analysis. Always false for non-PPR scoring modes.
65+
pub ppr_truncated: bool,
66+
pub ppr_forward_pushes: usize,
67+
pub ppr_backward_pushes: usize,
6168
}
6269

6370
#[derive(Serialize, Clone)]

0 commit comments

Comments
 (0)