Skip to content

Add LRU caching concept for SeqFetcher and HDP - #877

Open
Peter-J-Freeman wants to merge 2 commits into
regex_removalfrom
cache_sf_hdp
Open

Add LRU caching concept for SeqFetcher and HDP#877
Peter-J-Freeman wants to merge 2 commits into
regex_removalfrom
cache_sf_hdp

Conversation

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator

Implement in-process LRU cache decorators for the SeqFetcher and HGVS data provider (HDP) to reduce repeated lookups during validation.

The SeqFetcher cache stores repeated fetch_seq() requests. The HDP cache stores results from get_tx_identity_info(), get_tx_for_gene(), get_pro_ac_for_tx_ac(), get_tx_exons(), get_gene_info() and get_tx_mapping_options(). get_tx_for_region() was intentionally excluded because these genomic coordinate queries are unlikely to repeat during validation.

Both wrappers use the decorator pattern and transparently delegate all uncached methods to the underlying implementations.

Benchmarking shows the SeqFetcher cache primarily improves runtime stability, while the addition of the HDP cache provides a substantial reduction in overall validation time.

Refs #876

Implement in-process LRU cache decorators for the SeqFetcher and HGVS
data provider (HDP) to reduce repeated lookups during validation.

The SeqFetcher cache stores repeated fetch_seq() requests. The HDP cache
stores results from get_tx_identity_info(), get_tx_for_gene(),
get_pro_ac_for_tx_ac(), get_tx_exons(), get_gene_info() and
get_tx_mapping_options(). get_tx_for_region() was intentionally excluded
because these genomic coordinate queries are unlikely to repeat during
validation.

Both wrappers use the decorator pattern and transparently delegate all
uncached methods to the underlying implementations.

Benchmarking shows the SeqFetcher cache primarily improves runtime
stability, while the addition of the HDP cache provides a substantial
reduction in overall validation time.

Refs #876
@Peter-J-Freeman

Peter-J-Freeman commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

@John-F-Wagstaff. This is a concept, so feel free not to accept. I know you are careful with caches.

For reference, taken from the issues link

Benchmark methodology

To evaluate the impact of introducing in-process LRU caches, the full
VariantValidator test suite was executed three times for each caching
configuration using:

pytest -n 4

Each benchmark consisted of 2,197 passing tests and 6 skipped tests.
The same hardware, Python environment and xdist configuration were used
throughout. Three independent runs were performed for each
configuration, and the mean runtime, standard deviation (SD) and
coefficient of variation (CV) were calculated.

Three configurations were compared:

  1. No caching (baseline).

  2. SeqFetcher cache only, caching repeated fetch_seq() requests.

  3. SeqFetcher + HGVS Data Provider (HDP) cache.

The HDP cache stores the results of repeated calls to:

  • get_tx_identity_info()

  • get_tx_for_gene()

  • get_pro_ac_for_tx_ac()

  • get_tx_exons()

  • get_gene_info()

  • get_tx_mapping_options()

get_tx_for_region() was intentionally excluded because genomic
coordinate queries are expected to have a very low cache hit rate during
normal validation workflows.

Results

Configuration Run 1 (s) Run 2 (s) Run 3 (s) Mean (s) SD (s) CV (%) Δ Mean Δ SD
No cache 468.55 450.96 472.48 464.00 11.46 2.47
SeqFetcher cache 463.84 459.06 459.62 460.84 2.61 0.57 -0.68% -77.2%
SeqFetcher + HDP cache 412.18 435.23 415.53 420.98 12.56 2.98 -9.27% +9.6%

Conclusions

Caching repeated sequence retrievals alone provides only a modest
improvement in overall runtime (0.68%), but it substantially reduces
run-to-run variability, decreasing the standard deviation by
approximately 77%. This indicates that sequence retrieval is not a major
runtime bottleneck but that eliminating repeated fetches makes execution
more consistent.

The majority of the performance improvement comes from caching HGVS Data
Provider lookups. When the HDP cache is combined with the SeqFetcher
cache, the mean runtime falls from 464.00 seconds to 420.98
seconds
, representing a reduction of approximately 43 seconds
(9.27%).

These results suggest that repeated transcript and annotation lookups
are a significant component of VariantValidator execution time.
Specifically, caching transcript identity, transcript-to-gene
relationships, transcript exon structures, transcript mapping options,
protein accessions and gene information removes a substantial amount of
repeated database work during validation.

Although the combined cache exhibits similar run-to-run variability to
the uncached implementation, the overall reduction in execution time is
large enough to clearly demonstrate that the HGVS Data Provider cache is
responsible for the vast majority of the observed performance gains,
with the SeqFetcher cache providing a smaller complementary benefit by
avoiding repeated sequence retrievals.

#876

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 64.00000% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.76%. Comparing base (650b73a) to head (f56556b).
⚠️ Report is 3 commits behind head on regex_removal.

Files with missing lines Patch % Lines
VariantValidator/modules/vvMixinInit.py 64.00% 18 Missing ⚠️
Additional details and impacted files
@@                Coverage Diff                @@
##           regex_removal     #877      +/-   ##
=================================================
- Coverage          84.84%   84.76%   -0.08%     
=================================================
  Files                 48       48              
  Lines              14554    14601      +47     
=================================================
+ Hits               12348    12377      +29     
- Misses              2206     2224      +18     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

Everything mentioned here, including get_seq already goes through

https://github.com/openvar/vv_hgvs/blob/master/vvhgvs/dataproviders/interface.py

Which includes an lru cache for all of this.

Yet despite this the benefit of a bit more hits on the cash is enough to make this look like a performance gain, especially for SeqRepo.

I currently have no idea what would help for SeqRepo usage, aside from bumping the global cash size.

I added TranscriptMapData to help this issue (repeated re-calls of expensive DB fetches), it caches the first fetch it gets for each transcript/transcript-genomic ref pair, for the lifetime of the validation. This works on a per variant basis at the moment. It also currently only affects some get_tx_exons and get_tx_mapping_options but should probably be expanded to all, along with some other queries, suhch as get_tx_identity_info and possibly some others.

I have no idea how much expanding this would help in total VS creating an extra layer of local caching like this PR does, or upping the global cache size however, it did speed things up somewhat when I was testing it, even in circumstances that the global cash should have invalidated the gain.

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

So the conculusion is well it works, but we aren't fully sure why :)
Well, do we argue it or just go with it?

I am happy to change the cache sizes etc to see if I can make it even faster.

What do you think?

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

There will be a global removal of a SeqRepo call when a project to map Selenon genes properly concludes in a few weeks and we do the next database build. This will help a lot

@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

Not quite, I have suspicions of why, namely that if you do tests in parallel you might get enough fetches to overflow the cache between "related" calls, causing a re-fetch, the cache size is fixed.

When I said the global cache size I was talking about the cache in vv_hgvs, if this cache size is increased it should affect the validation +replace ref calls internal to vvhgvs too. If we get the same performance boost from that then we don't want to add another layer of caching.

@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

We also might see some changes to performance between using fetch_seq from the vvhgvs and pulling out the vvhgvs seq fetcher and then using it separate from that (i.e. we might be unintentionally end running around the cache with the way we use the seq fetcher VS doing hdp.seq_fech()).

Adding an extra layer of caching and a load of extra code to maintain to undo that if it is what is happening would be a waste.

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

cool so its an in vvta and seqrepo thing. But this shows its worth investigating so was not useless :)

@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

Who knows what happens if we bump the vvhgvs cache size though? it is probably worth checking.

The entire original vvhgvs code base was architected by someone who side-loaded a cache of correct answers in before running the tests! He did do some complex checks depending on cache state, but in the state we forked from it effectively defaulted to just testing the existing "pickled" cache unless steps were taken to prevent this. (and he also thought, or possibly still thinks, that even heavy users should fetch from remote servers like everyone else, but just cache harder, from the way thing are written at least. )

If you do want to do a test we need to change
hgvs.global_config.lru_cache.maxsize
either that or edit
vv_hgvs/vvhgvs/_data/defaults.ini
locally while running the tests. The original default is 100 distinct queries (per cached function I believe) so upping it to 200 should show some affect, if we get no affect then that is a result, but a low but real affect probably means that we are end-running around the cache for seqrepo but the rest is working.

@Peter-J-Freeman

Peter-J-Freeman commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

I prefer to do it in vvminininit to make it easier to maintain

        # --------------------------------------------------------------
        # HGVS global configuration
        # --------------------------------------------------------------

        vvhgvs.global_config.uta.pool_max = 25
        vvhgvs.global_config.formatting.max_ref_length = 1000000

Setting to 200. Do you want me to delete my local caches or keep them and see what happens, or both?

@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

I don't know what will happen if you change the settings, I don't know whether adding the extra lru cache on top will have any affect at all on performance with an increased underlying cache size.

So we have to try the combinations,

  • boosted underlying cache no overlying cache
  • boosted underlying cache seqrepo overlying cache
  • boosted underlying cache full overlying cache
    and see what happens. Then we can think about what that means. We might want to try changing the cache quantity too e.g. test 200 vs 400, (vs implicit test of 100, which is already tested as the default) but that depends on the result.

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

cool can do all this. By the way, pytest or pytest -n 4 for best results that are more realistic?

@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

whatever you did for your first tests for now, I think, so that we don't have to re-do a baseline.

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

I'm running this now. Just got started, but the results are shocking. Will publish when done

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

OK, this is really unexpected

Final benchmark table (slowest → fastest)

Configuration Run 1 (s) Run 2 (s) Run 3 (s) Mean (s) SD (s) CV (%) Δ Mean Δ SD
No cache (HGVS default) 468.55 450.96 472.48 464.00 11.46 2.47
SeqFetcher cache (HGVS default) 463.84 459.06 459.62 460.84 2.61 0.57 -0.68% -77.2%
SeqFetcher + HDP cache (HGVS default) 412.18 435.23 415.53 420.98 12.56 2.98 -9.27% +9.6%
SeqFetcher + HDP cache + HGVS LRU = 200 337.47 337.45 344.44 339.79 4.03 1.19 -26.77% -64.8%
SeqFetcher + HGVS LRU = 200 335.85 337.58 339.56 337.66 1.86 0.55 -27.23% -83.8%
SeqFetcher + HGVS LRU = 400 312.71 300.03 296.56 303.10 8.42 2.78 -34.68% -26.5%
SeqFetcher + HDP cache + HGVS LRU = 400 287.05 285.29 301.05 291.13 8.76 3.01 -37.26% -23.6%
SeqFetcher + HDP cache + HGVS LRU = 600 272.08 275.53 267.82 271.81 3.86 1.42 -41.42% -66.3%
SeqFetcher + HDP cache + HGVS LRU = 800 262.48 265.58 268.16 265.41 2.86 1.08 -42.80% -75.0%
SeqFetcher + HDP cache + HGVS LRU = 1000 251.92 254.46 254.83 253.74 1.59 0.63 -45.31% -86.1%

T

Caching clearly helps, but whats the sweet spot?

@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

What are the results without the SeqFetcher cache ?

We specifically need to know what happens with the vv_hgvs lru cache goes up without any VV local caching VS with or we can't tell wether it is worth adding the VV local caching.

The gain from adding the local cache VS running without the local cache should drop as the vv_hgvs lru cache goes up, but how much?

We need:

  • No VV cache, HGVS LRU = 200
  • No VV cache, HGVS LRU = 400
    and possibly
  • No VV cache, HGVS LRU = 1000

if the difference after a certain point is less than a couple of seconds out of more than 200 that changes things.

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

Yep, I ran out of time to do the runs with the local hdp cache on, and the local seqfetcher cache on, then both off. Will add these over the weekend. I decided to try max out the gain first to get a ball park, but didn't manage to, but it's all informative and will help get to a conclusion.

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

Table 2 – Follow-up benchmarking (evening session)

Configuration Run 1 (s) Run 2 (s) Run 3 (s) Mean (s) SD (s) CV (%) Δ Mean
Local HDP cache OFF, SeqFetcher cache OFF, HGVS LRU = 1000 330.31 330.35 332.62 331.09 1.30 0.39 -28.64%
SeqFetcher + HGVS LRU = 600 330.94 321.27 302.76 318.32 14.32 4.50 -31.40%
SeqFetcher + Local HDP cache + HGVS LRU = 1000 282.75 286.72 285.78 285.08 2.08 0.73 -38.56%
SeqFetcher + Local HDP cache + HGVS LRU = 1200 279.61 283.80 285.39 282.93 2.99 1.06 -39.02%
SeqFetcher + HGVS LRU = 1000 286.94 278.94 279.10 281.66 4.57 1.62 -39.30%
SeqFetcher + HGVS LRU = 1200 279.38 276.04 277.56 277.66 1.67 0.60 -40.16%

Summary

The second benchmarking session investigated the contribution of the HGVS internal LRU cache independently of the custom Local HDP and SeqFetcher wrapper caches. Increasing the HGVS internal cache continued to improve performance from 600 to 1000 entries, with a smaller additional improvement observed at 1200 entries.

Disabling both the Local HDP and SeqFetcher caches while retaining an HGVS cache size of 1000 increased the mean runtime to 331.09 s, demonstrating that the wrapper caches contribute additional performance improvements. However, comparison of the remaining configurations indicates that once the HGVS internal cache is increased to 1000–1200 entries, the majority of the performance benefit is attributable to the HGVS internal cache itself.

Within this benchmark session, the fastest configuration was:

  • SeqFetcher cache enabled
  • Local HDP cache disabled
  • HGVS internal LRU cache = 1200

This achieved a mean runtime of 277.66 s, corresponding to a 40.16% reduction relative to the uncached baseline measured in Session 1.

The difference between HGVS LRU = 1000 and HGVS LRU = 1200 was modest (281.66 s versus 277.66 s), indicating diminishing returns as the cache size increases. Based on these data, a cache size of approximately 1000–1200 entries per cached HGVS method appears to capture most of the achievable performance benefit for the VariantValidator functional test suite.

The benchmark also suggests that the additional Local HDP wrapper cache provides little or no measurable benefit once the HGVS internal cache has been increased to this range.

Configure the benchmark branch with the current optimal cache
configuration identified during performance testing.

Changes:
- Increase the HGVS internal LRU cache size to 1000 entries.
- Leave the SeqFetcher cache enabled.
- Disable the Local HDP cache wrapper.
- Retain the Local HDP cache implementation in the codebase for
  future benchmarking and evaluation.

Benchmarking indicates that an HGVS LRU cache size of 1000 provides
the best balance between execution speed and expected memory usage for
the current VariantValidator workload. Increasing the cache beyond
1000 yielded only marginal additional performance improvements.
@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

So if we intend to bump the cache to 1.2k or so we just want to boost the vvhgvs cache setting, and not pull this?

I should note that since the tests end up repeatedly target the same problem transcripts in different ways, we can not expect the same level of performance gain for user input as we get for tests (from either cache version), though some specific batch tests may gain a outsized benefit.

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

SeqFetcher cache benchmarking (HGVS LRU = 1000, Local HDP cache disabled)

SeqFetcher cache size Mean (s) SD (s) CV (%) Δ Mean
OFF 307.39 2.99 1.00
4096 261.71 0.91 0.36 -14.86%
8192 260.30 1.39 0.55 -15.31%
16384 257.87 1.52 0.61 -16.11%
32768 252.50 1.32 0.52 -17.86%
65536 249.68 2.83 1.13 -18.78%

Summary

The final phase of benchmarking evaluated the impact of the SeqFetcher LRU cache while keeping the HGVS internal LRU cache fixed at 1000 entries and the Local HDP cache disabled. Enabling the SeqFetcher cache immediately produced a substantial improvement in execution time, reducing the runtime by approximately 15% compared with having no SeqFetcher cache.

Increasing the cache size beyond 4096 continued to improve performance, although the magnitude of the improvement steadily decreased, demonstrating the expected diminishing returns of an LRU cache. Performance continued to improve through 8192, 16384, 32768, and 65536 entries, with the largest gains achieved at the smaller cache sizes and progressively smaller improvements thereafter.

The fastest configuration tested was 65536 entries, achieving an 18.78% reduction in runtime relative to the uncached configuration. However, the improvement over 32768 entries was relatively modest (approximately 2.8 s, or 0.9–1.1%), suggesting that the cache is approaching saturation for the VariantValidator functional test workload.

Overall, these benchmarks indicate that the SeqFetcher cache provides a significant performance benefit, while increasing the cache beyond 32768 entries yields only marginal additional improvement. Consequently, 32768 entries remains a sensible default configuration, providing near-optimal performance while avoiding unnecessary growth in cache size.

Agreed. We should not pull this. I will make a fresh cleaned branch, but wanted to add this in before doing it. I think the sf cache is optimal already too

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author
Rank Local HDP Cache SeqFetcher Cache HGVS Global Cache DBGet Cache Mean (s) Mean (min) Δ Mean
1 (Baseline) Off Off Off Off 1594.66 26.58 Baseline
2 Off On (32768) 1000 Off 280.78 4.68 −1313.88 s (−82.4%)
3 Off On (32768) 1000 On (10000) 259.15 4.32 −1335.51 s (−83.8%)
4 (Best) Off On (32768) 1000 On (15000) 251.60 4.19 −1343.06 s (−84.2%)

Summary

The final benchmarking demonstrates that the combined caching strategy provides a substantial improvement in VariantValidator performance. With all caches disabled, the complete test suite required an average of 26.58 minutes. Enabling the recommended caching configuration reduced this to 4.19 minutes, representing an overall reduction of 1343.06 seconds (22.38 minutes), or an 84.2% decrease in execution time.

The recommended cache configuration is therefore:

  • Local HGVS HDP cache: Disabled
  • SeqFetcher cache: Enabled (32,768 entries)
  • HGVS global cache: 1,000 entries
  • DBGet cache: Enabled (15,000 entries)

Further increases in cache size did not produce measurable improvements, indicating that these settings provide a good balance between execution speed and memory consumption for the VariantValidator validation workload.

@Peter-J-Freeman

Peter-J-Freeman commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

I should note that since the tests end up repeatedly target the same problem transcripts in different ways, we can not expect the same level of performance gain for user input as we get for tests (from either cache version), though some specific batch tests may gain a outsized benefit.

I agree. The test suite is something of a best-case scenario because it repeatedly validates the same genes and transcripts, allowing the caches to warm up and be reused. We therefore should not expect the same magnitude of improvement for a single user validation.

That said, I think there are several important benefits:

Individual validations should still improve because a single validation performs multiple repeated SeqFetcher, database and VVTA/HDP lookups internally, so there are opportunities for cache hits even within a single request.
Multi-worker execution also benefits. The benchmarking was performed using four pytest workers, demonstrating that the caching strategy remains effective under concurrent workloads. This is directly applicable to the REST API, where multiple worker processes are servicing requests simultaneously.
The REST API and batch processing are likely to benefit the most. These workflows frequently process many variants from the same gene or transcript, particularly when all-transcript or raw transcript reporting is enabled. Those workloads naturally generate repeated lookups, making them ideal candidates for this caching strategy.
The optimisation specifically targets some of the most expensive operations in the validation pipeline—database lookups, SeqRepo sequence retrieval and HGVS data provider access. Reducing repeated I/O is likely to provide more benefit than attempting to optimise code that is already computationally efficient.
The remaining major performance bottleneck is likely to be normalisation. Unlike the lookup stages, this is largely computational rather than I/O bound, so it will probably require a different optimisation strategy than caching.

Finally, one aspect I think is important is that this work was benchmark-driven rather than assumption-driven. We didn't simply add caches everywhere. Each cache was benchmarked, cache sizes were tuned empirically, and caches that showed little or no measurable benefit were either reduced to their optimum size or deliberately not implemented. The result is a caching strategy that balances execution speed, memory usage and maintainability, rather than simply maximising cache size or caching every possible lookup.

@Peter-J-Freeman

Copy link
Copy Markdown
Collaborator Author

An additional point worth noting is that the comparison against the last develop branch is conservative. Since that point, the project has gained a substantial number of new unit and regression tests, increasing the overall workload of the CI pipeline. Despite this, the end-to-end GitHub Actions runtime has reduced from approximately 36 minutes on develop to approximately 22 minutes on the final optimisation branch. This represents an improvement of almost 14 minutes (≈38%), even though the pipeline is now executing a larger and more comprehensive test suite.

In other words, the optimisation has not simply made the previous workload faster—it has made a larger workload complete in significantly less time. That improvement is the cumulative result of the codebase clean-up, removal of unnecessary regex and string processing, increased use of HGVS objects, reduced object creation, improved lookup paths, targeted caching, and benchmark-driven cache tuning. Together, these changes have improved maintainability, increased automated test coverage, and substantially reduced execution time for both local development and continuous integration.

I strongly belive that when you add your own optimisations @John-F-Wagstaff h, i.e. the fine-tooth-comb clean, we have done a really good job of this refactor. Also, test coverage is now just marginally below 85%.

@John-F-Wagstaff

Copy link
Copy Markdown
Collaborator

The very fact that the seqFetcher cache produces performance results like this however means that we are probably doing an end-run around the hdp inbuilt caching for sequence fetch. Otherwise the difference between no caching and the first cached test should probably be a lot smaller, though I would be more certain if you had set the cache size to 1000 for the first test not 4 times that.

This does allow us to have a higher seq fetch cache than the for the other endpoints though. Despite this should probably see a better results for the same cache size if the vvhgvs cache is also used by the VV, so in future we may want to add the provision to have a different vvhgvs internal cache size for seq fetch than the other endpoints, fix the accidental end run, and then remove the extra seq fetcher cache code from VV to avoid the unneeded extra complexity.

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.

2 participants