Skip to content

Commit

Permalink
Merge pull request #73 from NREL/ndr/lru-cache
Browse files Browse the repository at this point in the history
LRU Cache
  • Loading branch information
nreinicke authored Dec 13, 2023
2 parents a674e18 + 7dccd2f commit 1097f89
Show file tree
Hide file tree
Showing 15 changed files with 230 additions and 19 deletions.
26 changes: 25 additions & 1 deletion docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,30 @@ energy_rate_unit = "gallons_gasoline_per_mile"
ideal_energy_rate = 0.02857143
# A real world adjustment factor for things like temperature and auxillary loads
real_world_energy_adjustment = 1.166

# An optional cache that will keep track of recent energy values with the same input.
# If this is ommitted from the config, the model will compute the energy for every link in a search.
# See note below for some considerations when using this.
[traversal.vehicles.float_cache_policy]
cache_size = 10_000
key_precisions = [
2, # speed goes from 71.23 to 7123
4, # grade goes from 0.123 (decimal) to 1230 or 123 (millis) to 1230000
]
```

```{note}
When using the float cache it's possible that you might get slightly different final energy values versus the same query run with no caching.
The reason for this is how the input floating point values get converted into an integer for storage in the cache.
If your key precision for a grade value is 4, an incoming value of 0.123456 would get converted into 1234 and stored in the cache as such.
This is done to make sure we're actually getting cache hits and improving performance.
If we used a precision of 10, there might not be many other links in the road network that share the same exact properties at that resolution.
But, the tradeoff here is that if you used a key precision of 1, grade values of 0.14 and 0.05 would both result in the integer 1 being stored in the cache.
This would render grades of 5% and 14% to be equal to each other from an energy perspective and they are clearly not.
So, usage of this cache can result in improved runtimes for the energy traversal model but the user should make sure the precision values are appropriate for the application.
```

## Plugins
Expand Down Expand Up @@ -271,7 +295,7 @@ distance_unit = "meters"

The load balancer plugin estimates the runtime for each query. That information is used by `CompassApp` in order to best leverage parallelism.

For example, we have configured a parallelism of 2 and have 4 queries, but one query is a cross-country trip and will take a very long time to run.
For example, we have configured a parallelism of 2 and have 4 queries, but one query is a cross-country trip and will take a very long time to run.
With the load balancer plugin, Compass will identify this and bundle the three smaller queries together:

```
Expand Down
1 change: 1 addition & 0 deletions rust/routee-compass-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ geo = { workspace = true }
ordered-float = { workspace = true }
derive_more = "0.99.0"
priority-queue = "1.3.2"
lru = "0.12"
csv = { workspace = true }
kdam = { workspace = true }
log = { workspace = true }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::path::PathBuf;

use super::state::traversal_state::TraversalState;
use crate::util::cache_policy::cache_error::CacheError;
use crate::util::unit::UnitError;

#[derive(thiserror::Error, Debug)]
Expand All @@ -19,6 +20,8 @@ pub enum TraversalModelError {
InternalError(String),
#[error(transparent)]
TraversalUnitsError(#[from] UnitError),
#[error(transparent)]
CacheError(#[from] CacheError),
#[error("prediction model failed with error {0}")]
PredictionModel(String),
}
7 changes: 7 additions & 0 deletions rust/routee-compass-core/src/util/cache_policy/cache_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#[derive(thiserror::Error, Debug)]
pub enum CacheError {
#[error("Could not build cache policy due to {0}")]
BuildError(String),
#[error("Could not get value from cache due to {0}")]
RuntimeError(String),
}
133 changes: 133 additions & 0 deletions rust/routee-compass-core/src/util/cache_policy/float_cache_policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
use std::{num::NonZeroUsize, sync::Mutex};

use lru::LruCache;
use serde::{Deserialize, Serialize};

use super::cache_error::CacheError;

fn to_precision(value: f64, precision: i32) -> i64 {
let multiplier = 10f64.powi(precision as i32);
(value * multiplier).round() as i64
}

#[derive(Serialize, Deserialize)]
pub struct FloatCachePolicyConfig {
pub cache_size: usize,
pub key_precisions: Vec<i32>,
}

/// A cache policy that uses a float key to store a float value
/// The key is rounded to the specified precision.
///
/// # Example
///
/// ```
/// use routee_compass_core::util::cache_policy::float_cache_policy::{FloatCachePolicy, FloatCachePolicyConfig};
/// use std::num::NonZeroUsize;
///
/// let config = FloatCachePolicyConfig {
/// cache_size: 100,
/// key_precisions: vec![2, 2],
/// };
///
/// let cache_policy = FloatCachePolicy::from_config(config).unwrap();
///
/// // stores keys as [123, 235]
/// cache_policy.update(&[1.234, 2.345], 3.456).unwrap();
///
/// let value = cache_policy.get(&[1.234, 2.345]).unwrap().unwrap();
/// assert_eq!(value, 3.456);
///
/// // 1.233 rounds to 123
/// let value = cache_policy.get(&[1.233, 2.345]).unwrap().unwrap();
/// assert_eq!(value, 3.456);
///
/// // 1.2 rounds to 120
/// let value = cache_policy.get(&[1.2, 2.345]).unwrap();
/// assert_eq!(value, None);
///
/// // 2.344 rounds to 234
/// let value = cache_policy.get(&[1.234, 2.344]).unwrap();
/// assert_eq!(value, None);
pub struct FloatCachePolicy {
cache: Mutex<LruCache<Vec<i64>, f64>>,
key_precisions: Vec<i32>,
}

impl FloatCachePolicy {
pub fn from_config(config: FloatCachePolicyConfig) -> Result<Self, CacheError> {
let size = NonZeroUsize::new(config.cache_size).ok_or(CacheError::BuildError(
"maximum_cache_size must be greater than 0".to_string(),
))?;
let cache = Mutex::new(LruCache::new(size));
for precision in config.key_precisions.iter() {
if (*precision > 10) || (*precision < -10) {
return Err(CacheError::BuildError(
"key_precision must be betwee -10 and 10".to_string(),
));
}
}
Ok(Self {
cache,
key_precisions: config.key_precisions,
})
}

pub fn float_key_to_int_key(&self, key: &[f64]) -> Vec<i64> {
key.iter()
.zip(self.key_precisions.iter())
.map(|(v, p)| to_precision(*v, *p))
.collect()
}

pub fn get(&self, key: &[f64]) -> Result<Option<f64>, CacheError> {
let int_key = self.float_key_to_int_key(key);
let mut cache = self.cache.lock().map_err(|e| {
CacheError::RuntimeError(format!("Could not get lock on cache due to {}", e))
})?;
Ok(cache.get(&int_key).copied())
}

pub fn update(&self, key: &[f64], value: f64) -> Result<(), CacheError> {
let int_key = self.float_key_to_int_key(key);
let mut cache = self.cache.lock().map_err(|e| {
CacheError::RuntimeError(format!("Could not get lock on cache due to {}", e))
})?;
cache.put(int_key, value);
Ok(())
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_float_cache_policy() {
let config = FloatCachePolicyConfig {
cache_size: 100,
key_precisions: vec![1, 3],
};

let cache_policy = FloatCachePolicy::from_config(config).unwrap();

// should store keys as [12, 2345]
cache_policy.update(&[1.234, 2.345], 3.456).unwrap();

// same in same out
let value = cache_policy.get(&[1.234, 2.345]).unwrap().unwrap();
assert_eq!(value, 3.456);

// 1.15 rounds to 12
let value = cache_policy.get(&[1.15, 2.345]).unwrap().unwrap();
assert_eq!(value, 3.456);

// 1.14 rounds to 11
let value = cache_policy.get(&[1.14, 2.345]).unwrap();
assert_eq!(value, None);

// 2.344 rounds to 2344
let value = cache_policy.get(&[1.234, 2.344]).unwrap();
assert_eq!(value, None);
}
}
2 changes: 2 additions & 0 deletions rust/routee-compass-core/src/util/cache_policy/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod cache_error;
pub mod float_cache_policy;
1 change: 1 addition & 0 deletions rust/routee-compass-core/src/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod cache_policy;
pub mod conversion;
pub mod duration_extension;
pub mod fs;
Expand Down
9 changes: 1 addition & 8 deletions rust/routee-compass-powertrain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,4 @@ rayon = { workspace = true }

[features]
default = ["onnx"]
onnx = ["ort", "ureq", "flate2", "tar", "zip", "sha256"]

[build-dependencies]
sha256 = { version = "1.4", optional = true }
ureq = { version = "2.9", optional = true }
flate2 = { workspace = true, optional = true }
tar = { version = "0.4", optional = true }
zip = { version = "0.6", optional = true }
onnx = ["ort"]
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ mod tests {
EnergyRateUnit::GallonsGasolinePerMile,
None,
None,
None,
)
.unwrap();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ impl PredictionModel for OnnxSpeedGradeModel {
) -> Result<(EnergyRate, EnergyRateUnit), TraversalModelError> {
let (speed, speed_unit) = speed;
let (grade, grade_unit) = grade;

let speed_value: f32 = speed_unit.convert(speed, self.speed_unit).as_f64() as f32;
let grade_value: f32 = grade_unit.convert(grade, self.grade_unit).as_f64() as f32;
let array = ndarray::Array1::from(vec![speed_value, grade_value])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use std::{path::Path, sync::Arc};

use routee_compass_core::{
model::traversal::traversal_model_error::TraversalModelError,
util::unit::{EnergyRate, EnergyRateUnit, Grade, GradeUnit, Speed, SpeedUnit},
util::{
cache_policy::float_cache_policy::FloatCachePolicy,
unit::{EnergyRate, EnergyRateUnit, Grade, GradeUnit, Speed, SpeedUnit},
},
};

use super::{
Expand All @@ -15,14 +18,15 @@ use crate::routee::prediction::onnx::onnx_speed_grade_model::OnnxSpeedGradeModel

#[allow(clippy::too_many_arguments)]
pub fn load_prediction_model<P: AsRef<Path>>(
model_name: String,
name: String,
model_path: &P,
model_type: ModelType,
speed_unit: SpeedUnit,
grade_unit: GradeUnit,
energy_rate_unit: EnergyRateUnit,
ideal_energy_rate_option: Option<EnergyRate>,
real_world_energy_adjustment_option: Option<f64>,
cache: Option<FloatCachePolicy>,
) -> Result<PredictionModelRecord, TraversalModelError> {
let prediction_model: Arc<dyn PredictionModel> = match model_type {
ModelType::Smartcore => {
Expand Down Expand Up @@ -58,14 +62,15 @@ pub fn load_prediction_model<P: AsRef<Path>>(
let real_world_energy_adjustment = real_world_energy_adjustment_option.unwrap_or(1.0);

Ok(PredictionModelRecord {
name: model_name,
name,
prediction_model,
model_type,
speed_unit,
grade_unit,
energy_rate_unit,
ideal_energy_rate,
real_world_energy_adjustment,
cache,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ use std::sync::Arc;

use routee_compass_core::{
model::traversal::traversal_model_error::TraversalModelError,
util::unit::{
Distance, DistanceUnit, Energy, EnergyRate, EnergyRateUnit, EnergyUnit, Grade, GradeUnit,
Speed, SpeedUnit,
util::{
cache_policy::float_cache_policy::FloatCachePolicy,
unit::{
as_f64::AsF64, Distance, DistanceUnit, Energy, EnergyRate, EnergyRateUnit, EnergyUnit,
Grade, GradeUnit, Speed, SpeedUnit,
},
},
};

Expand All @@ -19,6 +22,7 @@ pub struct PredictionModelRecord {
pub energy_rate_unit: EnergyRateUnit,
pub ideal_energy_rate: EnergyRate,
pub real_world_energy_adjustment: f64,
pub cache: Option<FloatCachePolicy>,
}

impl PredictionModelRecord {
Expand All @@ -29,7 +33,26 @@ impl PredictionModelRecord {
distance: (Distance, DistanceUnit),
) -> Result<(Energy, EnergyUnit), TraversalModelError> {
let (distance, distance_unit) = distance;
let (energy_rate, _energy_rate_unit) = self.prediction_model.predict(speed, grade)?;

let energy_rate = match &self.cache {
Some(cache) => {
let key = vec![speed.0.as_f64(), grade.0.as_f64()];
match cache.get(&key)? {
Some(er) => EnergyRate::new(er),
None => {
let (energy_rate, _energy_rate_unit) =
self.prediction_model.predict(speed, grade)?;
cache.update(&key, energy_rate.as_f64())?;
energy_rate
}
}
}
None => {
let (energy_rate, _energy_rate_unit) =
self.prediction_model.predict(speed, grade)?;
energy_rate
}
};

let energy_rate_real_world = energy_rate * self.real_world_energy_adjustment;

Expand All @@ -39,6 +62,7 @@ impl PredictionModelRecord {
distance,
distance_unit,
)?;

Ok((energy, energy_unit))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,7 @@ mod tests {
EnergyRateUnit::GallonsGasolinePerMile,
Some(EnergyRate::new(0.02)),
Some(1.1252),
None,
)
.unwrap();
let charge_depleting_model_record = load_prediction_model(
Expand All @@ -310,6 +311,7 @@ mod tests {
EnergyRateUnit::KilowattHoursPerMile,
Some(EnergyRate::new(0.2)),
Some(1.3958),
None,
)
.unwrap();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use routee_compass_core::{
frontier::frontier_model_error::FrontierModelError, road_network::graph_error::GraphError,
traversal::traversal_model_error::TraversalModelError,
},
util::conversion::conversion_error::ConversionError,
util::{cache_policy::cache_error::CacheError, conversion::conversion_error::ConversionError},
};

#[derive(thiserror::Error, Debug)]
Expand Down Expand Up @@ -62,6 +62,8 @@ pub enum CompassConfigurationError {
#[error(transparent)]
ConversionError(#[from] ConversionError),
#[error(transparent)]
CacheError(#[from] CacheError),
#[error(transparent)]
TraversalModelError(#[from] TraversalModelError),
#[error(transparent)]
FrontierModelError(#[from] FrontierModelError),
Expand Down
Loading

0 comments on commit 1097f89

Please sign in to comment.