Skip to content

Float histograms: implement methods for Add/Sub operations using Kahan summation#15687

Merged
beorn7 merged 41 commits intoprometheus:mainfrom
crush-on-anechka:feature/kahan-summation-histograms
Feb 7, 2026
Merged

Float histograms: implement methods for Add/Sub operations using Kahan summation#15687
beorn7 merged 41 commits intoprometheus:mainfrom
crush-on-anechka:feature/kahan-summation-histograms

Conversation

@crush-on-anechka
Copy link
Contributor

@crush-on-anechka crush-on-anechka commented Dec 17, 2024

This PR addresses issue #14105 and PR #14325 maintained by @beorn7. While the issue has an assignee, I decided to contribute out of personal interest as there hasn’t been recent activity.

Overview:
New methods, kahanAdd and kahanSub, to the FloatHistogram replicate the logic of the existing Add and Sub methods but incorporate the Kahan summation algorithm. The methods require an initialized compensation histogram as input and return both the modified receiving histogram and the updated compensation histogram. The resulting histograms must then be combined using the original Add/Sub methods. Usage examples can be seen in the modified TestFloatHistogramAdd and TestFloatHistogramSub test cases.
 
Details:

  • I added two helper functions kahanSumDec and kahanSumInc (copied from promql/engine.go to avoid circular imports which handle additions and subtraction operations.
  • I added the NewCompensationHistogram method. It initializes a compensation histogram which matches the schema and custom bucket layout of the receiving histogram, however all positive and negative bucket values are initialized to zero. The spans of the compensation histogram are shared with the receiving histogram. This ensures both histograms maintain identical span layouts. Sharing span slices avoids unnecessary operations, as the receiving histogram's spans are modified during add/sub operations.
  • I created the kahanAddBuckets function as an enhanced version of AddBuckets. This function processes both the receiving histogram's buckets and the corresponding compensation histogram buckets. Similarly, I modified reduceResolution into kahanReduceResolution.
     
    Food for thought:
  • The h.reconcileZeroBuckets method, invoked in Add/Sub and kahanAdd/kahanSub, modifies the spans of the receiving histogram (it happens in Compact method to be precise) in some cases. To keep the compensation histogram in sync, its span slices are explicitly resized accordingly. However, if the receiving histogram's spans are reallocated, I reassign the compensation histogram's spans to ensure they point to the correct underlying array. I observed that existing test cases show spans only decrease in length, which avoids reallocation in most cases. If span lengths can not increase, a reallocation check can be omitted.
  • The Add method contains logic for processing the CounterResetHint, but the Sub method does not. Is this intentional, or could it be an oversight?
  • The reduceResolution function currently performs floating-point additions when merging buckets. Incorporating the Kahan summation algorithm here would add complexity but Is it worth it?
NONE

@beorn7 beorn7 self-requested a review December 17, 2024 17:45
@beorn7
Copy link
Member

beorn7 commented Dec 17, 2024

@KofClubs is also working on this (or maybe the work has stalled?). Just FYI.

@beorn7
Copy link
Member

beorn7 commented Dec 17, 2024

The Add method contains logic for processing the CounterResetHint, but the Sub method does not. Is this intentional, or could it be an oversight?

Yes, I know. (See the TODO here). I'm not sure
yet how to deal with negative counters in general (which wreaks havoc with the whole "only goes down if there is a counter reset" heuristics).

@crush-on-anechka crush-on-anechka force-pushed the feature/kahan-summation-histograms branch 2 times, most recently from 7790b58 to 7ada325 Compare December 17, 2024 18:05
@crush-on-anechka
Copy link
Contributor Author

@KofClubs is also working on this (or maybe the work has stalled?). Just FYI.

I know, I guess the work has stalled as there hasn’t been activity for a long time. If that's not the case, then I'm ok to step aside and not interfere. Otherwise I'm ready to keep on

@crush-on-anechka crush-on-anechka force-pushed the feature/kahan-summation-histograms branch 2 times, most recently from 786179b to 8cfa0d5 Compare December 17, 2024 22:33
@crush-on-anechka crush-on-anechka marked this pull request as ready for review December 17, 2024 22:52
@beorn7
Copy link
Member

beorn7 commented Dec 18, 2024

Thank you very much, I'll have a detailed look ASAP (which is probably not very soon, as I'm about to disappear into my holiday break). If anyone wants to review this in the meantime, be my guest.

I haven't heard anything from @KofClubs , so let's hope we don't have redundant work happening here.

@beorn7
Copy link
Member

beorn7 commented Jan 9, 2025

Back from my holiday break, but swamped by the backlog. I hope I'll get to this next week.

@KofClubs
Copy link
Contributor

KofClubs commented Jan 9, 2025

@crush-on-anechka @beorn7

I just received an email about @crush-on-anechka 's good work. I'm sorry that I put this work on hold due to the long office hours every day:(

I shall unassign the corresponding issue.

@beorn7
Copy link
Member

beorn7 commented Jan 16, 2025

Sorry, I caught a cold and got less done than planned. I need to ask for more patience. (And of course, if somebody else beats me to the review, be my guest.)

@beorn7
Copy link
Member

beorn7 commented Jan 22, 2025

The Add method contains logic for processing the CounterResetHint, but the Sub method does not. Is this intentional, or could it be an oversight?

That's because I still haven't made up my mind how to treat counter resets for subtraction. I'll add that once I have an idea.

The reduceResolution function currently performs floating-point additions when merging buckets. Incorporating the Kahan summation algorithm here would add complexity but Is it worth it?

Let's not do it for now. I don't expect the counts in the merged buckets to be hugely different, and usually we'll only add a few buckets anyway.

Copy link
Member

@beorn7 beorn7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much. Generally looks good.

Special thanks for all the careful thought you put into this.

It's a bit sad that we need to duplicate so much logic, but I guess it cannot be prevented most of the time. (For the few occasions where we can, I have left a comment.)

It would be good to already wire the new methods into PromQL (for addition, subtraction, and the sum aggregation etc, see more in later comment). This will give us a lot more test coverage (via the PromQL tests), and we could even easily try out a few things in the PromQL tests to prove that the Kahan summation actually helps.

@beorn7
Copy link
Member

beorn7 commented Jan 22, 2025

A few more thoughts:

We currently don't do Kahan summation for the float binary + or - operator. That would require more plumbing in the PromQL engine, and I would also doubt it's terribly useful because it's unlikely to chain a lot of those operations in practical use.

Kahan summation is currently only used in certain aggregations, the corresponding "over time" functions and some specific functions. Many of those only act on float samples. The only aggregations and functions we will need Kahan summation for histograms for will be:

  • sum aggregation
  • avg aggregation
  • sum_over_time function
  • avg_over_time function

As far as I can see, we do not need KahanSub for any of those. So we actually do not need those parts.

@crush-on-anechka
Copy link
Contributor Author

@beorn7 thank you for the comments! I will be able to proceed in a week, if that's fine

@crush-on-anechka crush-on-anechka force-pushed the feature/kahan-summation-histograms branch from 8cfa0d5 to ca0e4a2 Compare February 11, 2025 17:39
@crush-on-anechka
Copy link
Contributor Author

@beorn7 I fixed the issues. I also replaced Add with KahanAdd in engine.go/aggregation and corresponding functions as you mentioned, but I'm a little bit confused with how to handle errors in adding up the accumulated receiving histogram and compensation histograms (lines 3129 and 3161) since handleAggregationError demands metricName. The solution could be to store metric name in groupedAggregation so we would have access to them, but I'm not sure.

@beorn7
Copy link
Member

beorn7 commented Feb 11, 2025

Thanks for all the updates. I'll have a look ASAP.

@beorn7
Copy link
Member

beorn7 commented Feb 18, 2025

Still being caught is other things. Hopefully I'll get to this before this weeks ends.

@beorn7
Copy link
Member

beorn7 commented Feb 25, 2025

More apologies. I'm still drawn away from more substantial code reviews by other things. This is still on my list to do ASAP.

Copy link
Member

@beorn7 beorn7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for your patience and all the epic work.

This looks very good in general. Among all the comments, there are two substantial topics:

  1. Handling of the zero bucket (I assume that's not yet correct, see comment for details).
  2. Like for floats, we should first try to do simple average calculation and only revert to incremental average calculation if we run into overflow problems.

(About the 2nd point: Without Kahan summation, the incremental average calculation also helps to avoid the problem of adding a huge number (the running sum) to a small number (a new sample). However, Kahan summation solves that in a different way, so now the incremental calculation is only needed if the running sum would overflow float64. If that's not the case, it's just additional work and additional inaccuracies. See #14413 for some historical context.)

We need to address (1) in this PR, but you could leave TODOs about (2) if you feel it's too much for this PR.

Thanks to wiring the Kahan summation into PromQL already, we get some test coverage for free. However, let's add some more tests for histograms to make sure everything works as expected. All of these tests should be done via the PromQL testing framework, so they are easy to write (just using PromQL, no Go code required). You could take inspiration from float tests, e.g. https://github.com/prometheus/prometheus/blob/61aa82865d9c8474393bbbdcd539c58f63ba514f/promql/promqltest/testdata/aggregators.test#L612C1-L617C1 would not work without Kahan summation, https://github.com/prometheus/prometheus/blob/61aa82865d9c8474393bbbdcd539c58f63ba514f/promql/promqltest/testdata/aggregators.test#L576C1-L585C1 exercises the encremental avg calculation.

It would also be good to construct a test that exposes the (suspected) bug with the zero bucket, see point (1) above.

@crush-on-anechka
Copy link
Contributor Author

@beorn7 Thanks for the feedback! I'll analyze the information and update the PR asap

@crush-on-anechka crush-on-anechka force-pushed the feature/kahan-summation-histograms branch from 7655f25 to 9d82d2c Compare March 21, 2025 21:25
Copy link
Member

@beorn7 beorn7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here my thoughts. The zero bucket handling is really tough… WDYT?

@beorn7
Copy link
Member

beorn7 commented Mar 26, 2025

To go even further down the rabbit hole:

When reconciling zero buckets, we might be merging buckets into the zero bucket.

Even when ignoring zero buckets, we will still merge buckets if we sum up histograms of different resolution.

If we merge buckets, we add numbers. And we might need to add a very small to a very large number. So we should also use Kahan summation in this case, given that we have the compensation value available anyway.

However, now we don't add one number to another one with its compensation value, we add two numbers with each having their own compensation term. Let's say bucket b1 with c1, and bucket b2 with c2. We need to check the math, but I guess it will be something like first doing b1, c1 = kahansum.Inc(b2, b1, c1) and then c1 += c2.

For the merge between regular buckets, I guess this can easily be added to kahanReduceResolution. For the merge in case of reconciling zero buckets, it could go into the special kahanReconcileZeroBuckets hypothesized above.

@crush-on-anechka
Copy link
Contributor Author

@beorn7 regarding the rabbit hole - I see where you're coming from, I'll take care of it

Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
…liation, and resolution reduction

Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
…uction

Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
…ation term

Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
@crush-on-anechka crush-on-anechka force-pushed the feature/kahan-summation-histograms branch from 08ad456 to a54b370 Compare February 5, 2026 19:53
@crush-on-anechka crush-on-anechka requested review from a team and krajorama as code owners February 5, 2026 19:53
@crush-on-anechka crush-on-anechka force-pushed the feature/kahan-summation-histograms branch 3 times, most recently from 572fe99 to f62b9b6 Compare February 6, 2026 02:37
@crush-on-anechka
Copy link
Contributor Author

  • Removed KahanSub, cleaned up the leftover line, and added explanations for previously commented-out tests.
  • Added CounterResetHint field to the compensation histogram because some tests started failing after rebasing onto the current main.
  • Added an explanation for why the error returned from Add in aggregation is ignored.

histogram_avg_incremental_2:
The question why this test doesn't work stumped me. During another round of investigation, I found that it’s outcome appeared random due to the unpredictable order in which histograms are processed from the input series. The core issue was that Mul and Div operations (used in incremental average calculations) on high-magnitude floats caused rounding errors in insignificant digits which led to huge errors later on, as in our tests values are chosen to cancel each other out.
Example: intermediate values -2e+99 and 2.0000000000000002e+99, which should sum to 0, could instead produce a result of magnitude 1e86.
To fix this behavior I added RoundToSignificant() method which rounds the least significant digits of float values. It's applied only during incremental avg calculation. This fixes the high-magnitude float issues and allows histogram_avg_incremental_2 to pass.
But there are trade-offs: In certain scenarios, this rounding may slightly reduce accuracy without practical benefit.
Another edge case: averaging multiple values like 1.7976931348623157e+308 can result in +Inf when using RoundToSignificant() (without it, the calculation works correctly).
What do you think of this approach?

@beorn7
Copy link
Member

beorn7 commented Feb 6, 2026

My spontaneous thought about the rounding: This might work out in our (artificial) test cases, where we do expect round numbers, but in general, the rounding can only reduce accuracy rather than increasing it.

It's inevitable that the accuracy of the float operations depends in their order, and we have decided so far to not go down the rabbit hole and start to reorder operations for improved accuracy (see comments), so we have to accept that the accuracy is "randomly" depending on whatever order happens to be used.

@beorn7
Copy link
Member

beorn7 commented Feb 6, 2026

About the counter reset hint: Yes, we only recently improved the testing framework to actually check for the counter reset hint. This explains the test failures after the rebase.

Copy link
Member

@beorn7 beorn7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All looks good now, except the rounding. As already mentioned above, I have a really bad feeling about this. While I cannot provide an explicit proof, I'm pretty sure that we don't really improve accuracy by rounding. We are merely making it more likely to get "round" results, which might be the reason why our test cases (which expect round results) pass with the rounding.

The motivation for such a trick should be the other way around: We should only do it if we understand mathematically why we are doing it.

And since we don't, I propose to remove the rounding again and comment out the test cases that fail because of that, while adding an explanation that the precise result depends on the order of operation, which is unpredictable.

Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
@crush-on-anechka crush-on-anechka force-pushed the feature/kahan-summation-histograms branch from f62b9b6 to 831e5db Compare February 6, 2026 18:57
@crush-on-anechka
Copy link
Contributor Author

You’re right, I overlooked that the main goal here is to improve accuracy. Rounding intermediate results only to make an artificial test pass is not an effective approach.
I’ve removed the rounding path, commented out the failing test, and added an explanation describing why it doesn't work. The changes were squashed into the existing commits and force-pushed.

@beorn7
Copy link
Member

beorn7 commented Feb 6, 2026

Great. I'll give this a final look tomorrow, and then we are hopefully and finally done.

My apologies that this took so long, but I think we have learned a lot on the journey.

One final question: I appreciate your effort to keep the commits meaningful for reviewing. However, I would not like to merge the now 40 commits into main as-is. My idea is to just squash them all into one big "add Kahan summation for native histograms" commit. For example, we don't need to have the journey of adding a KahanSub and then removing it again in main. However, if you feel strongly about it, let me know and then we can check if there is a meaningful grouping of commits to do.

@crush-on-anechka
Copy link
Contributor Author

I’m totally fine with squashing commits into one on merge. The only reason I tried to keep them clean was to make the review easier.

Copy link
Member

@beorn7 beorn7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All good (as far as I can see). Just let's be a bit more generous with the comments on deactivated tests.

Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
Copy link
Member

@beorn7 beorn7 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉
Thanks again.

I'm currently re-running the tests. The failure seemed to have been a flaky test. Will merge on green.

@beorn7 beorn7 merged commit 1dcdb07 into prometheus:main Feb 7, 2026
53 of 54 checks passed
@crush-on-anechka
Copy link
Contributor Author

I received an email saying that the CI had failed, but I decided to leave it until the morning. Happy to have it all done, thank you for all your guidance!

bwplotka added a commit that referenced this pull request Feb 11, 2026
* Update npm dependencies for v3.10

Signed-off-by: Ganesh Vernekar <ganesh.vernekar@reddit.com>

* Fix critical npm vulnerabilities with npm audit fix

Signed-off-by: Ganesh Vernekar <ganesh.vernekar@reddit.com>

* UI: Move HistoryCompleteStrategy into its own file and fix lint

Signed-off-by: Ganesh Vernekar <ganesh.vernekar@reddit.com>

* [REFACTOR] Relabel: Remove unnecessary Process() function

All uses can be replaced by ProcessBuilder, which is more efficient.

Signed-off-by: Bryan Boreham <bjboreham@gmail.com>

* promql: use Kahan summation for Native Histograms (#15687)

As for float samples, Kahan summation is used for the `sum` and `avg` aggregation and for the respective `_over_time` functions.

Kahan summation is not perfect. This commit also adds tests that even Kahan summation cannot reliably pass.
These tests are commented out.

Note that the behavior might be different on other hardware platforms. We have to keep an eye on test failing on other hardware platforms and adjust them accordingly.

Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>

* fix(docs): typo in prometheus_agent.md doc

Signed-off-by: Mohammad Abbasi <mohammad.v184@gmail.com>

* AWS SD: ECS Standalone Tasks

The current ECS role in AWS SD assumes that a task is part of a service.
This means that tasks that are started as part of AWS Batch will get
missed and not be discovered. This changed fixes this so that standalone
tasks can be discovered as well.

Signed-off-by: matt-gp <small_minority@hotmail.com>

* AWS SD: Optmise MSK Role

Signed-off-by: matt-gp <small_minority@hotmail.com>

* promtool: support missing promql syntax features (#17926)

Namely promql-duration-expr and promql-extended-range-selectors. This
allows promtool to e.g. check rules files using syntax gated by these
features.

Signed-off-by: Ian Kerins <git@isk.haus>

* Also run CI on release tags

Make sure we also run the main CI workflow `v*` release tags
so `publish_release` job is run.

Signed-off-by: SuperQ <superq@gmail.com>

* chore(deps): bump github.com/hashicorp/consul/api from 1.32.0 to 1.33.2 (#17449)

Bumps [github.com/hashicorp/consul/api](https://github.com/hashicorp/consul) from 1.32.0 to 1.33.2.
- [Release notes](https://github.com/hashicorp/consul/releases)
- [Changelog](https://github.com/hashicorp/consul/blob/main/CHANGELOG.md)
- [Commits](hashicorp/consul@api/v1.32.0...api/v1.33.2)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/consul/api
  dependency-version: 1.33.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>

* AWS SD: Load Region Fallback (#18019)

* AWS SD: Load Region Fallback

---------

Signed-off-by: matt-gp <small_minority@hotmail.com>

* Build riscv64 docker image by default (#17508)

* Allow building riscv64 docker  image

Co-authored by: nijincheng@iscas.ac.cn;

Signed-off-by: ffgan <sudoemt@gmail.com>

* Update Makefile

Co-authored-by: Ben Kochie <superq@gmail.com>
Signed-off-by: ffgan <sudoemt@gmail.com>

---------

Signed-off-by: ffgan <sudoemt@gmail.com>
Co-authored-by: Ben Kochie <superq@gmail.com>

* Increase Renovate speed (#18052)

* Allow Renovate to run all day on the monthly day.
* Increase the max PRs from 10 to 20.
* Incresae the PR open rate from 2 to 5.

Signed-off-by: SuperQ <superq@gmail.com>

* docs(api): clarify metadata vs remote protocols (#17481)

Signed-off-by: György Krajcsovits <gyorgy.krajcsovits@grafana.com>

* tsdb/index: export sentinel error for symbol table size exceeded

Signed-off-by: Patryk Prus <p@trykpr.us>

* Update Go dependencies for v3.10 release

Signed-off-by: Ganesh Vernekar <ganesh.vernekar@reddit.com>

* Kubernetes SD: Disable WatchListClient in tests

Signed-off-by: Ganesh Vernekar <ganesh.vernekar@reddit.com>

* Replace deprecated google.CredentialsFromJSON with option.WithAuthCredentialsFile

Signed-off-by: Ganesh Vernekar <ganesh.vernekar@reddit.com>

* chore(deps): update actions/checkout action to v4.3.1 (#18054)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/cache action to v5.0.3 (#18053)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

* chore(deps): update actions/setup-go action to v6.2.0 (#18055)

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

---------

Signed-off-by: Ganesh Vernekar <ganesh.vernekar@reddit.com>
Signed-off-by: Bryan Boreham <bjboreham@gmail.com>
Signed-off-by: Aleksandr Smirnov <5targazer@mail.ru>
Signed-off-by: Mohammad Abbasi <mohammad.v184@gmail.com>
Signed-off-by: matt-gp <small_minority@hotmail.com>
Signed-off-by: Ian Kerins <git@isk.haus>
Signed-off-by: SuperQ <superq@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
Signed-off-by: ffgan <sudoemt@gmail.com>
Signed-off-by: György Krajcsovits <gyorgy.krajcsovits@grafana.com>
Signed-off-by: Patryk Prus <p@trykpr.us>
Co-authored-by: Ganesh Vernekar <ganesh.vernekar@reddit.com>
Co-authored-by: Bryan Boreham <bjboreham@gmail.com>
Co-authored-by: Sasha <103973965+crush-on-anechka@users.noreply.github.com>
Co-authored-by: Mohammad Abbasi <mohammad.v184@gmail.com>
Co-authored-by: matt-gp <small_minority@hotmail.com>
Co-authored-by: Ian Kerins <git@isk.haus>
Co-authored-by: SuperQ <superq@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Arve Knudsen <arve.knudsen@gmail.com>
Co-authored-by: Julien <291750+roidelapluie@users.noreply.github.com>
Co-authored-by: George Krajcsovits <krajorama@users.noreply.github.com>
Co-authored-by: ffgan <sudoemt@gmail.com>
Co-authored-by: Patryk Prus <p@trykpr.us>
Co-authored-by: Ganesh Vernekar <ganeshvern@gmail.com>
Co-authored-by: Joe Adams <github@joeadams.io>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants