Skip to content

Adds caching and some minor items: retries, max run time, max tokens, and extra body args - #765

Open
mitchcapper wants to merge 1 commit into
jehna:mainfrom
mitchcapper:caching_support
Open

Adds caching and some minor items: retries, max run time, max tokens, and extra body args#765
mitchcapper wants to merge 1 commit into
jehna:mainfrom
mitchcapper:caching_support

Conversation

@mitchcapper

Copy link
Copy Markdown

This caching is extremely low level, the cache is keyed on the key items we send to the server model, json mode, the context and the variable being renamed. This is fairly intentional to make it as flexible and narrow as possible. We cache right before we would otherwise make a server call and store it right after. The basic premise is ask an AI the exact same question with the same prompt and you will get the same answer, so why waste time and money to do it again.

This gets us several things for free:

  • Caching doesn't care about what file it came from, or even any of the surrounding code other than the 500 chars around the identifier itself. This means if code is repeated within a file, or even across files, as long as its formatting is the exact same it will be cached and only converted once.
  • Files that are partly modified but mostly the same will have the bulk of the code be cache hits
  • As this is largely seamless to the rest of humanify there is a fairly low chance this would break anything.
  • This is far less delicate than a pause and resume the cache state is written after each prompt comes back and even if a file had some minimal modifications it would still be able to use the cache
  • Right now the app crashes/throws an error/aborts for any reason and all work is lost. Nothing like converting 50K worth of identifiers in a file for it to bail out at 30K due to the AI server timing out or a server blip and losing all that work (and money and time). Caching seamlessly solves all of it.

Some key things to know:

  • The current code doesn't actually use already renamed variables to assist with the naming of the next variable. This might seem counter intuitive but I think the goal here is to avoid the AI tainting itself if it previously guessed wrong. If it starts naming things as it thinks its in an obfuscator then the variables in the context are named things related to being a obfuscator it guesses the next variable name thinking in the mind this must be an obfuscator program lets give it a name related to that. Turns out though it was wrong it was in a compression engine instead. It could also be a performance thing. This was actually a bit surprising to me given the serial action it takes (see parallelism note below).

  • This feature is off by default, you must specify a cache directory (--cache-dir cli arg or HUMANIFY_CACHE_DIR env var) for it to do anything, so existing behavior is unchanged. You can also force caching off on a request implicitly with --no-cache (no matter what env or other cli vars are used).

  • We could potentially allow the user more fine-grained access to what the cache key is. This could allow for different agents / models to name different parts. For now I scoped it as specific as possible so you could A/B test models against one file to see which is better. I did not include the --extra-body or --max-tokens options however, so you could minorly tweak a specific model (thinking off or different token limit) without invalidating the cache.

  • The trade off of minimal footprint means to most of the program caching doesn't exist and there is no special action for caching. For limited potential to corrupt the cache it uses single files rather than some central database. We can't just quickly load the entire progress for one file we literally probe the cache for every identifier. Cache hits require opening each of those files as well. This means resuming at an app that has already done 10K identifiers may take it a good 15-20 seconds (slower if your disk is slower as I am on an NVME) thats one identifier every 2ms but yes there are faster ways. The cache could be made more modular pretty easily as there are only a few key functions a cache backend would need to implement.

  • While the caching semi-touches a lot of files almost all the changes in the cli folder are just passing through additional args.

  • The cache uses hashes rather than storing the actual contents of the context window. This isn't so much for privacy as it is for speed / disk space but it also makes your cache directory not contain a super easy way to restore the contents of the original file.

Future parallelism support
Right now, the code is worse than the python GIL it operates largely as a serialized beast one variable at a time. Naming of each variable (in terms of what the AI gets) however is completely unrelated to all the other work done. Now we do things before we finalize a name to avoid collisions and other problems, however that is after the AI call. At the time of the AI call all we care about is the context around it from_the_original_source.

There may be many reasons for this, not to mention potential race conditions, but I see this as largely great news. With minimal work I think the code could operate in parallel without issue by doing one of two things:
*) Separate logic path that just gets all the AI responses for the renames calculating the context windows and such on its own. It would run this pass before anything else. This would involve changing a bit more of the code
*) Require the user to run the program twice (or internally just run our main loop twice). First run we disable or ignore whatever safety / collision / etc items on our caching run we are just caching all the ai responses and really dont care about the output we just run in parallel at whatever N multiple threads the user specifies. The second run we go back to our serial behavior, this is fine as now ai calls take sub 50ms to execute vs several seconds.

Other Changes
This commit does bundle other changes in with it. If desired I can try to separate them out so the caching PR is separate from the others. If you dont care though ill avoid the extra work.

The other changes are:

  • Better retry/error handling/ internal rationale tracking. Before things were pretty opaque: did a rename change something? Yes) success, No) for a variety of reasons an error. Technically the prompt intentionally asks the model to leave already-meaningful names unchanged, but previously failures and unchanged results were both collapsed into returning the original name. We now introduce RenameOutcome (Ok, Failed, Skipped) and dedicated observer events (rename_finished, rename_failed, rename_skipped) so unchanged successes are tracked properly without falsely counting as errors or resetting consecutive failure counters. Additionally, we add a new StrategyError::Permanent error classification for unrecoverable API failures (e.g. 401/403 auth errors, invalid model 4xx, malformed responses, exhausted ladder) so they are not fruitlessly retried. Rate limiting (429), timeouts (408), 5xx, and network drops remain StrategyError::Transient and are automatically retried with exponential backoff using --max-retries.

  • Max Tokens - most models allow us to give them a budget to work in, this does exactly that through the new '--max-tokens' option to help prevent model run away.

  • Global max runtime - If you want us to abort trying to rename any aliases after X amount of time you can use the new --max-run-seconds and we will stop making rename calls and just dump our current progress if we run out of time.

  • Extra body configuration for requests - The new --extra-body allows you to specify a json object to be merged with the request we are sending. This was mostly added to support disabling thinking. Sadly each provider is a bit unique in how you control this, so this seemed like the most generic way to go. It could allow for other tweaking though of provider specific settings without code modifications.

Oddly disabling thinking and putting a low token count for max tokens (only say a few hundred) actually gave me better results not just cheaper and faster. Granted I only tested a few files but I generally found the naming, while sometimes a bit more verbose, to be more accurate.

Without a doubt AI handled a good bit of this, I reviewed it but am new to the codebase and could have gotten something wrong.

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.

1 participant