Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(api): slightly faster simulation #9821

Merged
merged 6 commits into from
Apr 6, 2022
Merged

refactor(api): slightly faster simulation #9821

merged 6 commits into from
Apr 6, 2022

Conversation

amitlissack
Copy link
Contributor

@amitlissack amitlissack commented Mar 29, 2022

Overview

TLDR using caching to reduce simulation duration. These changes apply both to fast simulation and slow simulation.

While digging into #9503 I considered that maybe we're better off just improving the speed of slow simulation and doing away with fast simulation all together. Why? Because it seemed agonizing to have the PipetteDict produced by fast simulation match what is produced by the HC.

From profiling a Protocol that has thousands of aspirate/dispenses I learned that most of the SynchAdapter price was paid in the move_to function. It's called repeatedly. So I attempted to modify our InstrumentContextSimulation to have it inherit from InstrumentContextImplementation and ONLY override move_to. So basically, use the backward strategy from how fast sim had worked.

I spent a good deal of time profiling simulation and found sneaky little functions that are called zillions of times for no good reason. Comparing results on the PI I got:

  Slow Sim - 0:01:42.165820 
  New Fast sim - 0:00:56.065534
  Original Fast Sim - 0:00:33.754647

Pretty good, but not good enough.

So I fixed #9503 and am creating this PR to add the profiler learnings to improve simulation times.

Changelog

  • lru cache mathy functions. These are slow and called repeatedly.
  • Add thermocycler_present property to Deck. should_dodge_thermocycler is called zillions of times and does a linear search for thermocycler in deck map. We can cache this value.
  • BAD_PAIRS can be a set to avoid linear search

Review requests

Am I caching stuff that I shouldn't? Are the lru_cache limits reasonable?

I will add real life performance improvement metrics when I can.

Risk assessment

The thermocycler_present attribute is somewhat risky, though it is well covered by unit tests.
Otherewise. Low.

@amitlissack amitlissack requested review from a team and X-sam and removed request for a team March 29, 2022 20:33
@amitlissack amitlissack requested a review from a team as a code owner March 29, 2022 20:33
@codecov
Copy link

codecov bot commented Mar 29, 2022

Codecov Report

Merging #9821 (b70ae8e) into edge (6ac3fc1) will not change coverage.
The diff coverage is 100.00%.

Impacted file tree graph

@@           Coverage Diff           @@
##             edge    #9821   +/-   ##
=======================================
  Coverage   75.00%   75.00%           
=======================================
  Files        2007     2007           
  Lines       53286    53286           
  Branches     5148     5148           
=======================================
  Hits        39967    39967           
  Misses      12300    12300           
  Partials     1019     1019           
Flag Coverage Δ
app 71.52% <ø> (ø)
components 52.49% <ø> (ø)
notify-server 89.17% <ø> (ø)
protocol-designer 44.27% <ø> (ø)
step-generation 86.07% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Impacted Files Coverage Δ
api/src/opentrons/protocols/geometry/planning.py 90.52% <ø> (ø)
api/src/opentrons/hardware_control/pipette.py 88.08% <100.00%> (ø)
.../protocols/context/simulator/instrument_context.py 91.20% <100.00%> (ø)
api/src/opentrons/protocols/geometry/deck.py 93.06% <100.00%> (ø)

@amitlissack amitlissack force-pushed the api-sim-faster branch 2 times, most recently from 8625a1c to b834c5a Compare March 30, 2022 20:13
@amitlissack amitlissack marked this pull request as draft March 30, 2022 20:52
@amitlissack amitlissack changed the title WIP(api): faster simulation refactor(api): faster simulation Mar 31, 2022
@amitlissack amitlissack requested review from a team and LaconicPneumonic and removed request for X-sam and LaconicPneumonic March 31, 2022 12:40
@amitlissack amitlissack marked this pull request as ready for review March 31, 2022 12:45
@amitlissack amitlissack changed the title refactor(api): faster simulation refactor(api): slightly faster simulation Mar 31, 2022
def plunger_position(
self, instr: Pipette, ul: float, action: "UlPerMmAction"
) -> float:
mm = ul / instr.ul_per_mm(ul, action)
position = mm + instr.config.bottom
return round(position, 6)

@functools.lru_cache(PLUNGER_CACHE_LIMIT)
def plunger_speed(
self, instr: Pipette, ul_per_s: float, action: "UlPerMmAction"
) -> float:
mm_per_s = ul_per_s / instr.ul_per_mm(instr.config.max_volume, action)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ul_per_mm calls piecewise_volume_conversion which does a decent bit of math.

These plunger_X methods can be called thousands of times with very small variance in inputs. The config values are immutable so caching seems like a save move.

@@ -61,7 +60,7 @@ def should_dodge_thermocycler(

Returns True if we need to dodge, False otherwise
"""
if any([isinstance(item, ThermocyclerGeometry) for item in deck.values()]):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This method is called in each plan_moves which is called in each move_to. A needless search for a value that can be cached.

Copy link
Member

@sfoster1 sfoster1 left a comment

Choose a reason for hiding this comment

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

InstrumentHandler has the same lifetime as the hardware control api object and takes the attached instruments as inputs. That means that we need to clear the caches on the plunger position/speed/flowrate when the attached instruments change.

What might be a better idea is adding an LRU cache on hardware_control.pipettes.Pipette.ul_per_mm. Since that has the lifetime of an instrument, and it gets replaced when you change instruments, it won't need cache clearing, and it's where all the work happens.

I also think we could change the cache location for the deck slot key checking, but reasonable people can disagree.

api/src/opentrons/protocols/geometry/deck.py Show resolved Hide resolved
@amitlissack
Copy link
Contributor Author

amitlissack commented Mar 31, 2022

InstrumentHandler has the same lifetime as the hardware control api object and takes the attached instruments as inputs. That means that we need to clear the caches on the plunger position/speed/flowrate when the attached instruments change.

What might be a better idea is adding an LRU cache on hardware_control.pipettes.Pipette.ul_per_mm. Since that has the lifetime of an instrument, and it gets replaced when you change instruments, it won't need cache clearing, and it's where all the work happens.

I also think we could change the cache location for the deck slot key checking, but reasonable people can disagree.

I'm not sure I understand you regarding InstrumentHandler. If the attached instruments change won't the instr arg to the plunger methods be different hence creating a new cache entry?

I was hoping to do exactly what you suggested, but lru cache requires hashable arguments. ul_per_mm and _check_names fail that test. I will continue working on this.

@amitlissack
Copy link
Contributor Author

InstrumentHandler has the same lifetime as the hardware control api object and takes the attached instruments as inputs. That means that we need to clear the caches on the plunger position/speed/flowrate when the attached instruments change.
What might be a better idea is adding an LRU cache on hardware_control.pipettes.Pipette.ul_per_mm. Since that has the lifetime of an instrument, and it gets replaced when you change instruments, it won't need cache clearing, and it's where all the work happens.
I also think we could change the cache location for the deck slot key checking, but reasonable people can disagree.

I'm not sure I understand you regarding InstrumentHandler. If the attached instruments change won't the instr arg to the plunger methods be different hence creating a new cache entry?

I was hoping to do exactly what you suggested, but lru cache requires hashable arguments. ul_per_mm and _check_names fail that test. I will continue working on this.

I was wrong. It's piecewise_volume_conversion whose args cannot be hashed. Moving cache to ul_per_mm

Copy link
Contributor

@mcous mcous left a comment

Choose a reason for hiding this comment

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

Caching on a Deck method while the state of Deck can mutate underneath it makes me a little anxious. Is it necessary given the addition of the thermocycler_preset property?

@@ -75,6 +77,7 @@ def _assure_int(key: object) -> int:
else:
raise TypeError(type(key))

@functools.lru_cache(20)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this safe? It looks like self.data will mutate without busting the cache

Comment on lines 314 to 316
def __hash__(self) -> int:
"""A hash function as UserDict is unhashable."""
return id(self)
Copy link
Contributor

Choose a reason for hiding this comment

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

This feels iffy for the same reason the cache above feels iffy

@amitlissack
Copy link
Contributor Author

Caching on a Deck method while the state of Deck can mutate underneath it makes me a little anxious. Is it necessary given the addition of the thermocycler_preset property?

I had initially put the lru_cache on _assure_int which is static, potentially called thousands of times, and harmless to cache. I moved the cache to _check_names without thinking it through.

@amitlissack amitlissack requested review from sfoster1 and mcous April 5, 2022 14:11
Copy link
Member

@sfoster1 sfoster1 left a comment

Choose a reason for hiding this comment

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

Looks great to me!

@amitlissack amitlissack merged commit 3bffb41 into edge Apr 6, 2022
@amitlissack amitlissack deleted the api-sim-faster branch April 6, 2022 18:01
ecormany pushed a commit that referenced this pull request Apr 8, 2022
* cache thermocycler presense to avoid linear search

* cache check names

* comments. more caching.

* thermocycler_present attribute test and fix.

* constants.

* move lru_cache to ul_per_mm
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.

bug(papi-v2): "fast" analysis does not take tip volume into account
3 participants