Skip to content

Commit 8479eb7

Browse files
committed
Raise bucket weights to the power four in the historical model
Utilizing the results of probes sent once a minute to a random node in the network for a random amount (within a reasonable range), we were able to analyze the accuracy of our resulting success probability estimation with various PDFs across the historical and live-bounds models. For each candidate PDF (as well as other parameters, including the histogram bucket weight), we used the `min_zero_implies_no_successes` fudge factor in `success_probability` as well as a total probability multiple fudge factor to get both the historical success model and the a priori model to be neither too optimistic nor too pessimistic (as measured by the relative log-loss between succeeding and failing hops in our sample data). We then compared the resulting log-loss for the historical success model and selected the candidate PDF with the lowest log-loss, skipping a few candidates with similar resulting log-loss but with more extreme constants (such as a power of 11 with a higher `min_zero_implies_no_successes` penalty). Somewhat surprisingly (to me at least), the (fairly strongly) preferred model was one where the bucket weights in the historical histograms are exponentiated. In the current design, the weights are effectively squared as we multiply the minimum- and maximum- histogram buckets together before adding the weight*probabilities together. Here we multiply the weights yet again before addition. While the simulation runs seemed to prefer a slightly stronger weight than the 4th power we do here, the difference wasn't substantial (log-loss 0.5058 to 0.4941), so we do the simpler single extra multiply here. Note that if we did this naively we'd run out of bits in our arithmetic operations - we have 16-bit buckets, which when raised to the 4th can fully fill a 64-bit int. Additionally, when looking at the 0th min-bucket we occasionally add up to 32 weights together before multiplying by the probability, requiring an additional five bits. Instead, we move to using floats during our histogram walks, which further avoids some float -> int conversions because it allows for retaining the floats we're already using to calculate probability. Across the last handful of commits, the increased pessimism more than makes up for the increased runtime complexity, leading to a 40-45% pathfinding speedup on a Xeon Silver 4116 and a 25-45% speedup on a Xeon E5-2687W v3.
1 parent fa0beaf commit 8479eb7

File tree

1 file changed

+95
-46
lines changed

1 file changed

+95
-46
lines changed

lightning/src/routing/scoring.rs

Lines changed: 95 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,27 +1156,25 @@ fn three_f64_pow_9(a: f64, b: f64, c: f64) -> (f64, f64, f64) {
11561156
/// Given liquidity bounds, calculates the success probability (in the form of a numerator and
11571157
/// denominator) of an HTLC. This is a key assumption in our scoring models.
11581158
///
1159-
/// Must not return a numerator or denominator greater than 2^31 for arguments less than 2^31.
1160-
///
11611159
/// `total_inflight_amount_msat` includes the amount of the HTLC and any HTLCs in flight over the
11621160
/// channel.
11631161
///
11641162
/// min_zero_implies_no_successes signals that a `min_liquidity_msat` of 0 means we've not
11651163
/// (recently) seen an HTLC successfully complete over this channel.
11661164
#[inline(always)]
1167-
fn success_probability(
1165+
fn success_probability_float(
11681166
total_inflight_amount_msat: u64, min_liquidity_msat: u64, max_liquidity_msat: u64,
11691167
capacity_msat: u64, params: &ProbabilisticScoringFeeParameters,
11701168
min_zero_implies_no_successes: bool,
1171-
) -> (u64, u64) {
1169+
) -> (f64, f64) {
11721170
debug_assert!(min_liquidity_msat <= total_inflight_amount_msat);
11731171
debug_assert!(total_inflight_amount_msat < max_liquidity_msat);
11741172
debug_assert!(max_liquidity_msat <= capacity_msat);
11751173

11761174
let (numerator, mut denominator) =
11771175
if params.linear_success_probability {
1178-
(max_liquidity_msat - total_inflight_amount_msat,
1179-
(max_liquidity_msat - min_liquidity_msat).saturating_add(1))
1176+
((max_liquidity_msat - total_inflight_amount_msat) as f64,
1177+
(max_liquidity_msat - min_liquidity_msat).saturating_add(1) as f64)
11801178
} else {
11811179
let capacity = capacity_msat as f64;
11821180
let min = (min_liquidity_msat as f64) / capacity;
@@ -1199,6 +1197,57 @@ fn success_probability(
11991197
let (max_v, amt_v, min_v) = (max_pow + max_norm / 256.0, amt_pow + amt_norm / 256.0, min_pow + min_norm / 256.0);
12001198
let num = max_v - amt_v;
12011199
let den = max_v - min_v;
1200+
(num, den)
1201+
};
1202+
1203+
if min_zero_implies_no_successes && min_liquidity_msat == 0 {
1204+
// If we have no knowledge of the channel, scale probability down by a multiple of ~82%.
1205+
// Note that we prefer to increase the denominator rather than decrease the numerator as
1206+
// the denominator is more likely to be larger and thus provide greater precision. This is
1207+
// mostly an overoptimization but makes a large difference in tests.
1208+
denominator = denominator * 78.0 / 64.0;
1209+
}
1210+
1211+
(numerator, denominator)
1212+
}
1213+
1214+
#[inline(always)]
1215+
/// Identical to [`success_probability_float`] but returns integer numerator and denominators.
1216+
///
1217+
/// Must not return a numerator or denominator greater than 2^31 for arguments less than 2^31.
1218+
fn success_probability(
1219+
total_inflight_amount_msat: u64, min_liquidity_msat: u64, max_liquidity_msat: u64,
1220+
capacity_msat: u64, params: &ProbabilisticScoringFeeParameters,
1221+
min_zero_implies_no_successes: bool,
1222+
) -> (u64, u64) {
1223+
debug_assert!(min_liquidity_msat <= total_inflight_amount_msat);
1224+
debug_assert!(total_inflight_amount_msat < max_liquidity_msat);
1225+
debug_assert!(max_liquidity_msat <= capacity_msat);
1226+
1227+
let (numerator, denominator) =
1228+
if params.linear_success_probability {
1229+
let (numerator, mut denominator) =
1230+
(max_liquidity_msat - total_inflight_amount_msat,
1231+
(max_liquidity_msat - min_liquidity_msat).saturating_add(1));
1232+
1233+
if min_zero_implies_no_successes && min_liquidity_msat == 0 &&
1234+
denominator < u64::max_value() / 78
1235+
{
1236+
// If we have no knowledge of the channel, scale probability down by a multiple of ~82%.
1237+
// Note that we prefer to increase the denominator rather than decrease the numerator as
1238+
// the denominator is more likely to be larger and thus provide greater precision. This is
1239+
// mostly an overoptimization but makes a large difference in tests.
1240+
denominator = denominator * 78 / 64
1241+
}
1242+
1243+
(numerator, denominator)
1244+
} else {
1245+
// We calculate the nonlinear probabilities using floats anyway, so just stub out to
1246+
// the float version and then convert to integers.
1247+
let (num, den) = success_probability_float(
1248+
total_inflight_amount_msat, min_liquidity_msat, max_liquidity_msat, capacity_msat,
1249+
params, min_zero_implies_no_successes
1250+
);
12021251

12031252
// Because our numerator and denominator max out at 0.0078125 we need to multiply them
12041253
// by quite a large factor to get something useful (ideally in the 2^30 range).
@@ -1210,16 +1259,6 @@ fn success_probability(
12101259
(numerator, denominator)
12111260
};
12121261

1213-
if min_zero_implies_no_successes && min_liquidity_msat == 0 &&
1214-
denominator < u64::max_value() / 78
1215-
{
1216-
// If we have no knowledge of the channel, scale probability down by a multiple of ~82%.
1217-
// Note that we prefer to increase the denominator rather than decrease the numerator as
1218-
// the denominator is more likely to be larger and thus provide greater precision. This is
1219-
// mostly an overoptimization but makes a large difference in tests.
1220-
denominator = denominator * 78 / 64
1221-
}
1222-
12231262
(numerator, denominator)
12241263
}
12251264

@@ -1765,7 +1804,7 @@ mod bucketed_history {
17651804
// Because the first thing we do is check if `total_valid_points` is sufficient to consider
17661805
// the data here at all, and can return early if it is not, we want this to go first to
17671806
// avoid hitting a second cache line load entirely in that case.
1768-
total_valid_points_tracked: u64,
1807+
total_valid_points_tracked: f64,
17691808
min_liquidity_offset_history: HistoricalBucketRangeTracker,
17701809
max_liquidity_offset_history: HistoricalBucketRangeTracker,
17711810
}
@@ -1775,7 +1814,7 @@ mod bucketed_history {
17751814
HistoricalLiquidityTracker {
17761815
min_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
17771816
max_liquidity_offset_history: HistoricalBucketRangeTracker::new(),
1778-
total_valid_points_tracked: 0,
1817+
total_valid_points_tracked: 0.0,
17791818
}
17801819
}
17811820

@@ -1786,7 +1825,7 @@ mod bucketed_history {
17861825
let mut res = HistoricalLiquidityTracker {
17871826
min_liquidity_offset_history,
17881827
max_liquidity_offset_history,
1789-
total_valid_points_tracked: 0,
1828+
total_valid_points_tracked: 0.0,
17901829
};
17911830
res.recalculate_valid_point_count();
17921831
res
@@ -1809,12 +1848,15 @@ mod bucketed_history {
18091848
}
18101849

18111850
fn recalculate_valid_point_count(&mut self) {
1812-
self.total_valid_points_tracked = 0;
1851+
let mut total_valid_points_tracked = 0;
18131852
for (min_idx, min_bucket) in self.min_liquidity_offset_history.buckets.iter().enumerate() {
18141853
for max_bucket in self.max_liquidity_offset_history.buckets.iter().take(32 - min_idx) {
1815-
self.total_valid_points_tracked += (*min_bucket as u64) * (*max_bucket as u64);
1854+
let mut bucket_weight = (*min_bucket as u64) * (*max_bucket as u64);
1855+
bucket_weight *= bucket_weight;
1856+
total_valid_points_tracked += bucket_weight;
18161857
}
18171858
}
1859+
self.total_valid_points_tracked = total_valid_points_tracked as f64;
18181860
}
18191861

18201862
pub(super) fn writeable_min_offset_history(&self) -> &HistoricalBucketRangeTracker {
@@ -1900,20 +1942,23 @@ mod bucketed_history {
19001942
let mut actual_valid_points_tracked = 0;
19011943
for (min_idx, min_bucket) in min_liquidity_offset_history_buckets.iter().enumerate() {
19021944
for max_bucket in max_liquidity_offset_history_buckets.iter().take(32 - min_idx) {
1903-
actual_valid_points_tracked += (*min_bucket as u64) * (*max_bucket as u64);
1945+
let mut bucket_weight = (*min_bucket as u64) * (*max_bucket as u64);
1946+
bucket_weight *= bucket_weight;
1947+
actual_valid_points_tracked += bucket_weight;
19041948
}
19051949
}
1906-
assert_eq!(total_valid_points_tracked, actual_valid_points_tracked);
1950+
assert_eq!(total_valid_points_tracked, actual_valid_points_tracked as f64);
19071951
}
19081952

19091953
// If the total valid points is smaller than 1.0 (i.e. 32 in our fixed-point scheme),
19101954
// treat it as if we were fully decayed.
1911-
const FULLY_DECAYED: u16 = BUCKET_FIXED_POINT_ONE * BUCKET_FIXED_POINT_ONE;
1955+
const FULLY_DECAYED: f64 = BUCKET_FIXED_POINT_ONE as f64 * BUCKET_FIXED_POINT_ONE as f64 *
1956+
BUCKET_FIXED_POINT_ONE as f64 * BUCKET_FIXED_POINT_ONE as f64;
19121957
if total_valid_points_tracked < FULLY_DECAYED.into() {
19131958
return None;
19141959
}
19151960

1916-
let mut cumulative_success_prob_times_billion = 0;
1961+
let mut cumulative_success_prob = 0.0f64;
19171962
// Special-case the 0th min bucket - it generally means we failed a payment, so only
19181963
// consider the highest (i.e. largest-offset-from-max-capacity) max bucket for all
19191964
// points against the 0th min bucket. This avoids the case where we fail to route
@@ -1926,16 +1971,18 @@ mod bucketed_history {
19261971
// max-bucket with at least BUCKET_FIXED_POINT_ONE.
19271972
let mut highest_max_bucket_with_points = 0;
19281973
let mut highest_max_bucket_with_full_points = None;
1929-
let mut total_max_points = 0; // Total points in max-buckets to consider
1974+
let mut total_weight = 0;
19301975
for (max_idx, max_bucket) in max_liquidity_offset_history_buckets.iter().enumerate() {
19311976
if *max_bucket >= BUCKET_FIXED_POINT_ONE {
19321977
highest_max_bucket_with_full_points = Some(cmp::max(highest_max_bucket_with_full_points.unwrap_or(0), max_idx));
19331978
}
19341979
if *max_bucket != 0 {
19351980
highest_max_bucket_with_points = cmp::max(highest_max_bucket_with_points, max_idx);
19361981
}
1937-
total_max_points += *max_bucket as u64;
1982+
total_weight += (*max_bucket as u64) * (*max_bucket as u64)
1983+
* (min_liquidity_offset_history_buckets[0] as u64) * (min_liquidity_offset_history_buckets[0] as u64);
19381984
}
1985+
debug_assert!(total_weight as f64 <= total_valid_points_tracked);
19391986
// Use the highest max-bucket with at least BUCKET_FIXED_POINT_ONE, but if none is
19401987
// available use the highest max-bucket with any non-zero value. This ensures that
19411988
// if we have substantially decayed data we don't end up thinking the highest
@@ -1944,40 +1991,39 @@ mod bucketed_history {
19441991
let selected_max = highest_max_bucket_with_full_points.unwrap_or(highest_max_bucket_with_points);
19451992
let max_bucket_end_pos = BUCKET_START_POS[32 - selected_max] - 1;
19461993
if payment_pos < max_bucket_end_pos {
1947-
let (numerator, denominator) = success_probability(payment_pos as u64, 0,
1994+
let (numerator, denominator) = success_probability_float(payment_pos as u64, 0,
19481995
max_bucket_end_pos as u64, POSITION_TICKS as u64 - 1, params, true);
1949-
let bucket_prob_times_billion =
1950-
(min_liquidity_offset_history_buckets[0] as u64) * total_max_points
1951-
* 1024 * 1024 * 1024 / total_valid_points_tracked;
1952-
cumulative_success_prob_times_billion += bucket_prob_times_billion *
1953-
numerator / denominator;
1996+
let bucket_prob = total_weight as f64 / total_valid_points_tracked;
1997+
cumulative_success_prob += bucket_prob * numerator / denominator;
19541998
}
19551999
}
19562000

19572001
for (min_idx, min_bucket) in min_liquidity_offset_history_buckets.iter().enumerate().skip(1) {
19582002
let min_bucket_start_pos = BUCKET_START_POS[min_idx];
19592003
for (max_idx, max_bucket) in max_liquidity_offset_history_buckets.iter().enumerate().take(32 - min_idx) {
19602004
let max_bucket_end_pos = BUCKET_START_POS[32 - max_idx] - 1;
1961-
// Note that this multiply can only barely not overflow - two 16 bit ints plus
1962-
// 30 bits is 62 bits.
1963-
let bucket_prob_times_billion = (*min_bucket as u64) * (*max_bucket as u64)
1964-
* 1024 * 1024 * 1024 / total_valid_points_tracked;
2005+
let mut bucket_weight = (*min_bucket as u64) * (*max_bucket as u64);
2006+
bucket_weight *= bucket_weight;
2007+
debug_assert!(bucket_weight as f64 <= total_valid_points_tracked);
2008+
19652009
if payment_pos >= max_bucket_end_pos {
19662010
// Success probability 0, the payment amount may be above the max liquidity
19672011
break;
1968-
} else if payment_pos < min_bucket_start_pos {
1969-
cumulative_success_prob_times_billion += bucket_prob_times_billion;
2012+
}
2013+
2014+
let bucket_prob = bucket_weight as f64 / total_valid_points_tracked;
2015+
if payment_pos < min_bucket_start_pos {
2016+
cumulative_success_prob += bucket_prob;
19702017
} else {
1971-
let (numerator, denominator) = success_probability(payment_pos as u64,
2018+
let (numerator, denominator) = success_probability_float(payment_pos as u64,
19722019
min_bucket_start_pos as u64, max_bucket_end_pos as u64,
19732020
POSITION_TICKS as u64 - 1, params, true);
1974-
cumulative_success_prob_times_billion += bucket_prob_times_billion *
1975-
numerator / denominator;
2021+
cumulative_success_prob += bucket_prob * numerator / denominator;
19762022
}
19772023
}
19782024
}
19792025

1980-
Some(cumulative_success_prob_times_billion)
2026+
Some((cumulative_success_prob * (1024.0 * 1024.0 * 1024.0)) as u64)
19812027
}
19822028
}
19832029
}
@@ -3575,9 +3621,12 @@ mod tests {
35753621
// Now test again with the amount in the bottom bucket.
35763622
amount_msat /= 2;
35773623
// The new amount is entirely within the only minimum bucket with score, so the probability
3578-
// we assign is 1/2.
3579-
assert_eq!(scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, &params, false),
3580-
Some(0.5));
3624+
// we assign is around 41%.
3625+
let probability =
3626+
scorer.historical_estimated_payment_success_probability(42, &target, amount_msat, &params, false)
3627+
.unwrap();
3628+
assert!(probability >= 0.41);
3629+
assert!(probability < 0.42);
35813630

35823631
// ...but once we see a failure, we consider the payment to be substantially less likely,
35833632
// even though not a probability of zero as we still look at the second max bucket which

0 commit comments

Comments
 (0)