-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
rust: define shared Commit
type
#4351
Changes from all commits
84d6175
1459d88
f52200f
5e16ea3
997ddfa
103ccbd
734d780
d7fddfa
76bcca2
9ba9726
aef800e
dd3e955
532ea1a
c577558
8b11660
e7a6f6b
b41f47f
3c7e13c
ec0cf2c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); | ||
you may not use this file except in compliance with the License. | ||
You may obtain a copy of the License at | ||
|
||
http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
Unless required by applicable law or agreed to in writing, software | ||
distributed under the License is distributed on an "AS IS" BASIS, | ||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
See the License for the specific language governing permissions and | ||
limitations under the License. | ||
==============================================================================*/ | ||
|
||
//! Shared state for sampled data available to readers. | ||
|
||
use std::collections::HashMap; | ||
use std::sync::RwLock; | ||
|
||
use crate::proto::tensorboard as pb; | ||
use crate::reservoir::Basin; | ||
use crate::types::{Run, Step, Tag, WallTime}; | ||
|
||
/// Current state of in-memory sampled data. | ||
/// | ||
/// A commit is an internally mutable structure. All readers and writers should keep a shared | ||
/// reference to a single commit. When writers need to update it, they grab an exclusive lock to | ||
/// the contents. | ||
/// | ||
/// Deadlock safety: any thread should obtain the outer lock (around the hash map) before an inner | ||
/// lock (around the run data), and should obtain at most one `RunData` lock at once. | ||
#[derive(Debug, Default)] | ||
pub struct Commit { | ||
pub runs: RwLock<HashMap<Run, RwLock<RunData>>>, | ||
} | ||
|
||
impl Commit { | ||
/// Creates a new, empty commit. | ||
pub fn new() -> Self { | ||
Commit::default() | ||
} | ||
} | ||
|
||
/// Data for a single run. | ||
/// | ||
/// This contains all data and metadata for a run. For now, that data includes only scalars; | ||
/// tensors and blob sequences will come soon. | ||
#[derive(Debug, Default)] | ||
pub struct RunData { | ||
/// The time of the first event recorded for this run. | ||
/// | ||
/// Used to define an ordering on runs that is stable as new runs are added, so that existing | ||
/// runs aren't constantly changing color. | ||
pub start_time: Option<WallTime>, | ||
|
||
/// Scalar time series for this run. | ||
pub scalars: TagStore<ScalarValue>, | ||
} | ||
|
||
pub type TagStore<V> = HashMap<Tag, TimeSeries<V>>; | ||
|
||
#[derive(Debug)] | ||
pub struct TimeSeries<V> { | ||
/// Summary metadata for this time series. | ||
pub metadata: Box<pb::SummaryMetadata>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just wondering, what's the rationale for this being There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You’re right that it’s not that bad. Here are the ones that I’m Type size information
So yes, “large amount of data” can definitely go higher than 104 bytes, Boxing all three of these together improved end-to-end performance by There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, it was primarily a curiosity question - fine by me to leave it boxed. I didn't realize it'd be 24 bytes per Vec aka per String so it does add up to more than I'd have thought; at least one reason is that in my head I'm still thinking about immutable string types where there isn't an extra 8 bytes for capacity. I doubt that it makes a big difference in our case, but in general it would seem like some applications with a lot of immutable strings would save space overhead by having an immutable owned string representation with pointer + length only? I guess that must just not come up very often since I don't see much evidence that this exists. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right: you basically have:
where in all case the string layout is the same as the vector So if you want “immutable owned string with no capacity penalty”, you Some protobuf-like APIs use this form: iirc, capnproto arrays aren’t Rust also does have fixed-size arrays, so a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And, of course, not all languages with immutable strings fit them into 8 >>> import sys
>>> sys.getsizeof("")
49 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting, I think It hadn't occurred to me since |
||
|
||
/// Reservoir basin for data points in this time series. | ||
/// | ||
/// See [`TimeSeries::valid_values`] for a client-friendly view that omits `DataLoss` points | ||
/// and transposes `Step`s into the tuple. | ||
pub basin: Basin<(WallTime, Result<V, DataLoss>)>, | ||
} | ||
|
||
impl<V> TimeSeries<V> { | ||
/// Creates a new time series from the given summary metadata. | ||
pub fn new(metadata: Box<pb::SummaryMetadata>) -> Self { | ||
TimeSeries { | ||
metadata, | ||
basin: Basin::new(), | ||
} | ||
} | ||
|
||
/// Gets an iterator over `self.values` that omits `DataLoss` points. | ||
pub fn valid_values(&self) -> impl Iterator<Item = (Step, WallTime, &V)> { | ||
self.basin | ||
.as_slice() | ||
.iter() | ||
.filter_map(|(step, (wall_time, v))| Some((*step, *wall_time, v.as_ref().ok()?))) | ||
} | ||
} | ||
|
||
/// A value in a time series is corrupt and should be ignored. | ||
/// | ||
/// This is used when a point looks superficially reasonable when it's offered to the reservoir, | ||
/// but at commit time we realize that it can't be enriched into a valid point. This might happen | ||
/// if, for instance, a point in a scalar time series has a tensor value containing a string. We | ||
/// don't care too much about what happens to these invalid values. Keeping them in the commit as | ||
/// `DataLoss` tombstones is convenient, and [`TimeSeries::valid_values`] offers a view that | ||
/// abstracts over this detail by only showing valid data. | ||
#[derive(Debug)] | ||
pub struct DataLoss; | ||
|
||
/// The value of a scalar time series at a single point. | ||
#[derive(Debug, Copy, Clone, PartialEq)] | ||
pub struct ScalarValue(pub f64); | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_valid_values() { | ||
let mut ts = TimeSeries::<&str>::new(Box::new(pb::SummaryMetadata::default())); | ||
|
||
let mut rsv = crate::reservoir::StageReservoir::new(10); | ||
let wall_time = WallTime::new(0.0).unwrap(); // don't really care | ||
rsv.offer(Step(0), "zero"); | ||
rsv.offer(Step(1), "one"); | ||
rsv.offer(Step(2), "two"); | ||
rsv.offer(Step(3), "three"); | ||
rsv.offer(Step(5), "five"); | ||
rsv.commit_map(&mut ts.basin, |s| { | ||
(wall_time, if s == "three" { Err(DataLoss) } else { Ok(s) }) | ||
}); | ||
|
||
assert_eq!( | ||
ts.valid_values().collect::<Vec<_>>(), | ||
vec![ | ||
(Step(0), wall_time, &"zero"), | ||
(Step(1), wall_time, &"one"), | ||
(Step(2), wall_time, &"two"), | ||
// missing: Step(3) | ||
(Step(5), wall_time, &"five") | ||
] | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -70,6 +70,19 @@ impl Borrow<str> for Tag { | |
} | ||
} | ||
|
||
/// The name of a TensorBoard run. | ||
/// | ||
/// Run names are derived from directory names relative to the logdir, but are lossily converted to | ||
/// valid Unicode strings. | ||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] | ||
pub struct Run(pub String); | ||
|
||
impl Borrow<str> for Run { | ||
fn borrow(&self) -> &str { | ||
&self.0 | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
@@ -85,6 +98,17 @@ mod tests { | |
assert_eq!(m.get("xent"), None); | ||
} | ||
|
||
#[test] | ||
fn test_run_hash_map_str_access() { | ||
use std::collections::HashMap; | ||
let mut m: HashMap<Run, i32> = HashMap::new(); | ||
m.insert(Run("train".to_string()), 1); | ||
m.insert(Run("test".to_string()), 2); | ||
// We can call `get` given only a `&str`, not an owned `Run`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you said on the other PR that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Correct. To start, the docs for Probably the most foolproof way to see what’s happening is to look at pub fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq, So here we’re calling Does this help? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, thanks! I didn't read far enough down the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No problem! Pointing you to the right docs is a good use of my time. :-) |
||
assert_eq!(m.get("train"), Some(&1)); | ||
assert_eq!(m.get("val"), None); | ||
} | ||
|
||
#[test] | ||
fn test_wall_time() { | ||
assert_eq!(WallTime::new(f64::INFINITY), None); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I've wanted for a while to opportunistically parse this from the filename so we don't have to open a file for each run in order to serve the initial list of runs, which is especially bad for remote filesystems. WDYT? Wouldn't be implemented here of course, but might affect the semantics we describe for this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That’d be fine with me, yep. I think that fits naturally into the logdir
loader structure.