Memoization Library#165
Conversation
…o eliminate O(N) bottlenecks
memoization is applied per (function name, arity). You can enable memoization for (func $x) while leaving (func $x $y) un-memoized. The caches are distinct so the two arities do not interfere. the memo system supports multi-answer (this I added as a feature after reviewing tabling prolog supports) with it supporting non-deterministic predicates by caching answer sets and replaying them to later callers. It also supports variant (non-ground) keys so non-ground calls can be keyed/normalized. I have tried to add this kind of info in the doc. Thank you. I can also add more tests if needed. |
Thank you. I will see what I can do. |
Hey, I further reviewed your concern and I have added the support where when memoizing the user can pass not only the function name but also arity length. I have further added tests and updated the doc. Thank you again. |
|
Thank you so much! Little style question: Last real concern: why is Else your PR seems ready, thank you, I will carefully review it and merge it in if I see no further issue! |
Hey, I did it with number because it's easier to type and more concise and less error prone for complex functions. It's also easy to parse. But maybe (tabling doesn't support this) with tabling version we could aspire to support partial memoization. We can memoize say (add 5 $y) where concrete arguments can be part of the key. This would help to memoize functions that are expensive only for some arguments and are cheap for the rest. We could save a lot on memory if we can bare the implementation burden. Overall it's just preference I could implement it either way. Let me know which you prefer.
On this, when I was first implementing the annotator (ae1514c), I had memoize function defined in spaces.pl (before I refactored it to its own library) and I was calling it like: 'memoize!'(Fun, 'Empty') :-
findall(Term, (translated_from(_, Term), Term = [=, [Fun|_], with this since I was querying translated_from without checking if a function existed, I was getting "Unknown procedure" when I had an empty predicate which necessitated dynamic declaration to return empty list in that case instead of failing. But now since I am checking if functions exist first before any memoization and raising an error if not: 'memoize'(Fun, 'Empty') :-
( atom(Fun), fun(Fun)
-> true
; throw(error(domain_error(function_symbol, Fun), 'memoize!/2'))
),dynamic is no longer needed and I can safely remove it. Thank you very much for pointing it out, I will push the commit now. |
|
Agreed. Specialized memoization becomes possible then. But for now I think that complexity is not needed. As it is easy to just define another function that passes a constant to another. So overall, it looks like your PR is ready. |
|
Amazing PR btw., this is gigantic! |
Thank you very much. Please let me know if it introduces any side effects at your earliest convenience as some changes touch the src modules. |
|
Is it possible to make your mem library work (speed up instead of slow down) for the following case? ;; Import lib_memo
!(import! &self (library lib_memo))
;; Define fromNumber to convert a number into a natural number
(= (fromNumber $n) (if (<= $n 0) Z (S (fromNumber (- $n 1)))))
;; Define less than or equal comparision operator for natural numbers
(= (lte Z Z) True)
(= (lte Z (S $y)) True)
(= (lte (S $x) Z) False)
(= (lte (S $x) (S $y)) (lte $x $y))
;; Memoize lte
!(memoize lte)
;; Test lte
!(lte (fromNumber 1000) (fromNumber 1000))
!(lte (fromNumber 1000) (fromNumber 1000))Or is it hopeless? |
Hey memoization works when there are identical function calls with in the recursive calls. But in this, every call is unique going from 1000 to 0. There is no subproblem whose result could be used for the call of another. Memoizing such function will only lead to overhead caused by cache miss and frequent evictions. For an input of a thousand here is the memoization stat |
|
Subsequent calls of |
Sorry I was memoizing fromNumber instead of lte. One thing till then you can try is increasing the unique limit and size limit |
Hey I had been working on the memoization with ring buffer queue I was able to see some performance gain but still not that much to have an effect. So till then you can try out this branch https://github.com/DagmawiKK/PeTTa/tree/feat/builtin_peano_support |
That looks totally awesome, I'm impatient to try. Thank you! |
Thank you very much. There might be some edge cases for which it might not work and also some futher optimization that can be done. Please let me know if you find it useful then we can futher improve it. |
|
Unfortunately it freezes some of my code. Anyway, I really like the idea and will provide you with a minimal test case to reproduce the problem, likely next week if it is still there. |
|
Also, I suppose, if it is possible, it might be better as a library than a core functionality, as not everybody assumes that a nested expression of |
Ya, I expected as much. There wasn't that many tests to cover every case. And ya, I would appreciate that. You cld also send them to me on mattermost. Thank you. |
I actually implemented it as library first but the optimization, if we also dont want to change syntax (as done in Lean too, they do it with compile time optimization and kernel reduction) can only be done at translation time. We need to differentiate function by their type and args and optimize accordingly if they use peano numbers. And this cant be done from a non interfering library. For usage, we can add an optin flag (which it supports partially via |
|
I am slightly worried of a user being puzzled by the outcome of But I suppose it can either:
|
|
BTW, it would be awesome if arithmetic operators are overloaded and optimized for Nat, but I suppose it is planned. |
Ya, the current implementation is just a proof of concept while I see if I can optimize the memoization lib. But I will come up with something that covers more edge cases if it's actually useful. |
|
BTW, the following code fails, but, anyway, it is probably the wrong place to discuss that. Maybe you could create a draft PR for it and we can discuss that over there. In any case it will next week for me. |
Memoization Engine & Runtime Extension Points
Summary
This PR introduces a comprehensive memoization engine with configurable eviction strategies (LRU, WTinyLFU), variant-key support for non-ground calls, multi-answer caching, and runtime extension hooks that enable external modules to intercept function dispatch and lifecycle events.
Features
1. Memoization Engine (
lib/lib_memo.pl)A thread-safe, policy-driven memoization system with:
answer-limitanswers per cache keyConfiguration Options
strategywtinylfuwtinylfuorlruunique-limitsize-limitfloatanswer-limitaggregatenonenone|min|max|sum|countUsage
2. Function Lifecycle Hooks (
ext_points.pl)Extension points for runtime integration:
metta_try_dispatch_call/4: Allows external modules to intercept runtime calls (e.g., for memoization dispatch)metta_on_function_changed/1: Triggered when a function is added/modified — enables automatic cache invalidationmetta_on_function_removed/1: Triggered when a function is removed — cleans up memoization statePerformance Comparison
Benchmark results comparing memoized vs non-memoized execution:
Fibonacci (fib)
Newton's Method (energy)
Strategy Comparison (fib, 800 entries)
Changes by File
src/ext_points.pl(NEW)src/filereader.plmetta_on_function_changed/1when registering functionssrc/main.plEmptyfrom CLI outputsrc/metta.plext_pointsmodule at startupsrc/spaces.pltranslated_from/2as dynamicsrc/specializer.plmetta_on_function_removed/1inforget_symbol/1src/translator.plresolve_runtime_call/4helper for dispatch interceptionIntegration
Memoization API
Extension Points (for library authors)
New Tests
The following test files validate memoization behavior:
examples/test_memo_stats.metta— Basic hit/miss trackingexamples/test_memo_aggregate.metta— Aggregation modesexamples/test_memo_multi_answer.metta— Multi-answer cachingexamples/test_memo_variant_nonground.metta— Variant-key supportexamples/test_memo_dependency_invalidation.metta— Dependency-aware invalidationRecommendations
unique-limitto slightly exceed expected distinct inputsBreaking Changes
None. This PR adds new functionality without removing existing behavior.