Monitoring for budget pressure, prover abuse#90
Conversation
Adds nudges to the agent to give up/try something new; refuse to run the prover on "let's just try a timeout again" case
jar-ben
left a comment
There was a problem hiding this comment.
Left some comments. The high-level approaches here look good; I think both the budget constraints and the check for "looping over the same prover input" can be helpful (and I think it would solve the real issue we have seen).
|
|
||
| if len(state['prover_history']) > 0: | ||
| last_item = state["prover_history"][-1] | ||
| if last_item["sort"] == "run" and any(i == "TIMEOUT" for (_,i) in last_item) and last_item["spec_digest"] == spec_hash: |
There was a problem hiding this comment.
Why only timeouts? verified rules, violated rules, sanity failures, .., are also not transient
There was a problem hiding this comment.
that's a great point actually.
|
|
||
| if len(state['prover_history']) > 0: | ||
| last_item = state["prover_history"][-1] | ||
| if last_item["sort"] == "run" and any(i == "TIMEOUT" for (_,i) in last_item) and last_item["spec_digest"] == spec_hash: |
There was a problem hiding this comment.
last_item["spec_digest"] == spec_hash is it possible in the current (or intended future) workflow that the .spec remains the same, but the .conf or .sol files changed? In such situations, a rerun makes sense.
There was a problem hiding this comment.
yeah, this assumes the only thing the agent can change is the spec. That's true currently, but will change. we'll have to digest the conf and/or VFS once those changes land.
| ) | ||
|
|
||
| stuck_rules = { | ||
| k: v for (k,v) in result.raw_rule_status.items() if v in ("TIMEOUT", "ERROR", "SANITY_FAILED") |
There was a problem hiding this comment.
"SANITY_FAILED" is sometimes the expected and "right" result, especially for parametric rules/invariants if there is a method that is intended to always revert (in such case, the whole rule/invariant is also flagged with SANITY_FAILED. I am not sure autoprover can currently fix this, and nagging with "You may need to significantly change your approach, or skip the property if this is a persistent issue" might just drop the rule completely.
There was a problem hiding this comment.
I'm writing this comment from memory, but I thought there was something here that didn't nag about rules that are expected to fail. If a rule's "correct" outcome is a failure (whether sanity fail or whatever), the agent should mark those rules as expected to fail, which will suppress this nag.
If we don't skip "expected to fail" rules in the nagging, that's something I need to fix.
There was a problem hiding this comment.
I just double checked, and we ignore the "expected to fail" state when considering nags; that's a bug
| """The author's monitor: budget wrap-up takes precedence; otherwise the | ||
| usual reminders-channel drain. On the (single) turn the budget warning | ||
| fires any pending reminders are dropped — moot, since the warning tells | ||
| the agent to ignore prover/feedback outcomes anyway.""" |
There was a problem hiding this comment.
It might be good to propagate to the reporting part (or at least log) that we dropped validations.
There was a problem hiding this comment.
🤦 of course, why didn't I think of this.
ericeil
left a comment
There was a problem hiding this comment.
I like the use of context vars for this; it feels like a good fix for "ambient" data that would be a pain to pass around explicitly everywhere. I left some naive comments, but overall the mechanism looks good to me.
| later phases.""" | ||
| curr = _cost_centers.get() | ||
| if curr is not None: | ||
| raise RuntimeError("Not good") |
There was a problem hiding this comment.
Should we use a more descriptive message here?
There was a problem hiding this comment.
I forgot about this (obvious placeholder) message lol, I'll fix it
| prev_accum = _budget_accumulator.set(pool) | ||
| try: | ||
| yield None | ||
| finally: |
There was a problem hiding this comment.
(I'm just learning about @contextmanager now - what an odd convention! - so forgive me if this question is nonsense): isn't the finally implied here? Is this the idiomatic way to write these?
There was a problem hiding this comment.
Get ready for a crash course on context managers!
A context manager is just an object which defines two special functions __enter__ and __exit__.
When you write:
with some_context_manager as x:
python calls __enter__ on some_context_manager; the value returned by that function is used to bind to x (roughly, I'm glossing over/partially misremembering some details)
When the context block is exited (normally, or via exception) it calls the special __exit__ method. If there is an exception being propagated, those are passed into the __exit__ function.
So, what does @contextmanager do?
Well, now we need to understand generators. If the keyword yield appears anywhere in a function, python transforms it into a generator object. A generator object exposes three methods, two of which are important to us, send and throw. On the first call to send, the body of the function begins executing up until the first yield statement. When it reaches said yield, execution of the generator suspends, and the yielded value is returned from send(). If you call send(something) on the generator again, something becomes the value returned from the yield expression, and execution of the generator resumes from the yield until the function exits, or another yield is hit.
The eagle eyed reader (which you are) will notice these are just kotlin coroutines; it is left as an exercise to the reader how you could implement generators on top of kotlin coroutines. Fun bonus fact: before async became first class, it was all bolted on top of this generator api.
The dual of send is throw, instead of having the most recent yield return a value, you have it throw the passed in exception.
You can now probably guess how @contextmanager works. It roughly looks like this:
class ContextManagerImpl:
wrapped: Generator
def __enter__(self):
return self.wrapped.send(None)
def __exit__(self, exception):
if exception:
self.wrapped.throw(exception)
else:
self.wrapped.send(None) where wrapped is the generator returned by calling the function decorated with @contextmanager. I'm glossing over some details (you can suppress exceptions during __exit__ for example) but this is the basic shape of it.
Now with all that background we can understand why we need finally. Without it, if the with block is exited with an exception, the yield statement in named_budget would throw, and that would immediately terminate any further execution in the body which would in turn skip the cleanup statements. By wrapping the yield in try: ... finally: ... we can ensure that any cleanup we need to do happens even if the with block terminates exceptionally.
TL;DR - isn't the finally implied here? -> No.
| # to — and feels pressure from — the pool directly. | ||
| prev_accum = _budget_accumulator.set(pool) | ||
| try: | ||
| yield None |
There was a problem hiding this comment.
Is there a difference between yield None and the bare yield you use elsewhere?
There was a problem hiding this comment.
no. can fix to be consistent
|
|
||
|
|
||
| # Pricing tables transcribed from Anthropic + OpenAI rate cards. | ||
| # Sources should be re-checked when new model families ship. |
There was a problem hiding this comment.
Is there no way to query for the current rates programmatically?
There was a problem hiding this comment.
(Oh I see this is existing code copied from elsewhere)
There was a problem hiding this comment.
It's a good question, and the answer is: not really. There are 3rd party efforts to catalog this stuff into some standardized format. Might be worth looking into.
Uses the monitoring feature of Certora/graphcore#24 to add two monitors:
Budget cancellation is co-operative, if the agent is overbudget/under budget pressure, it throws a special exception which is interpreted as the appropriate "I give up" type (only for the verification tasks).
As mentioned, there is a per cost center budget and a total "overall run" budget. The sum of cost center budgets does not necessarily have to be the overall budget, in fact, it is expected to be greater. This was chosen over a strict allocation of the total budget to different centers, with some arcane budget rebalancing for unused costs.
As part of this, and as predicted by @shellygr, we have moved the pricing information out of the display subsystem into the
llm/module. We also more accurately track 1hr vs 5m cache costs, as this matters when computing budget pressure.