Conversation
|
A rendered version is at https://github.com/sims1253/stan-design-docs/blob/master/designs/0035-stanbin-binary-output.md |
| | 12 | 4 | uint32 | Flags (`0` in v1; reserved for extensions) | | ||
| | 16 | 8 | uint64 | Number of rows (draws) | | ||
| | 24 | 8 | uint64 | Number of columns (parameters) | | ||
| | 32 | 4 | uint32 | Data section offset in bytes (`64 + names_size` in v1) | |
There was a problem hiding this comment.
We will want to avoid unaligned reads in the data section, so I think it will be important to at least 8-byte align the start of the data section. So this will be something like 64 + ((names_size + 7) / 8) * 8
|
|
||
| 2. Should stanbin become the default output format in a future CmdStan release, or remain opt-in indefinitely? | ||
|
|
||
| 3. Is the trailing metadata section (raw CSV comment text) the right long-term metadata representation, or should v1 adopt a structured format (e.g., key-value pairs) from the start? |
There was a problem hiding this comment.
My thought is either start as structured key-value metadata, or cut it entirely, but I'm mostly ambivalent (cmdstanpy can just stick to reading this information from the json version cmdstan can provide)
There was a problem hiding this comment.
imo this is also a good opportunity to have the metadata go to a separate file completly. So we would write a little json file with all the metadata.
There was a problem hiding this comment.
Curious if there is some kind of consensus here. I thought having it as one file was desired. 2 files would actually be easier to implement :D My first draft of this actually used a separate file for the metadata.
There was a problem hiding this comment.
We already have an argument called save_cmdstan_config which creates a json file equivalent of the opening comments. The adaptation metadata is also saved elsewhere, so it’s really only timing that is still available only as a comment, and we’d also like to change that (stan-dev/stan#3340)
| void operator()(const std::vector<double>& state) override { | ||
| write_row(state); | ||
| ++num_rows_; | ||
| update_rows_in_header(num_rows_); |
There was a problem hiding this comment.
You say earlier:
A reader that wants to attempt partial recovery from an incomplete file can compute the number of usable rows from the file size
but here you show the header being modified each write. I think the text earlier is much better: the rows in the header should just be set to the intended number of rows, to avoid needing to constantly seek back and forth.
If we really want to avoid a number of rows that is not 'true', it should be moved to finalization instead and left as 0 for the intermediary steps.
| # Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
|
||
| 1. Not human-readable: Unlike CSV, stanbin files cannot simply be inspected with a text editor. Users must use provided reader functions. |
There was a problem hiding this comment.
[note] a hex editor would be usable in a pinch
|
|
||
| 1. Not human-readable: Unlike CSV, stanbin files cannot simply be inspected with a text editor. Users must use provided reader functions. | ||
|
|
||
| 2. Tool ecosystem: Existing tools (stansummary, arviz, etc.) expect CSV format. These would need updates to support stanbin. |
There was a problem hiding this comment.
Note: I think it will also be important to provide a new cmdstan/bin/stanbin2csv that converts these files into more-or-less the normal stan csvs
|
|
||
| ## To resolve before merging this RFC | ||
|
|
||
| 1. Should the diagnostic file also support stanbin output in v1, or should it remain CSV/JSON only? The current proposal excludes it, but reviewers may want a unified binary path. |
There was a problem hiding this comment.
Inasmuch as the API proposed here matches the writer interface, I think we should be able to get this 'for free'/with very minimal additional effort
|
|
||
| 1. Should the diagnostic file also support stanbin output in v1, or should it remain CSV/JSON only? The current proposal excludes it, but reviewers may want a unified binary path. | ||
|
|
||
| 2. Should stanbin become the default output format in a future CmdStan release, or remain opt-in indefinitely? |
There was a problem hiding this comment.
I think that whatever the default is in cmdstanr/cmdstanpy will end up being the most used thing, regardless
|
|
||
| 2. Finalization edge cases: The writer rewrites the header on successful close. The behavior when sampling is interrupted (e.g., SIGINT during warmup) should be tested to confirm that readers can detect and recover from incomplete files via `metadata_offset == 0`. | ||
|
|
||
| 3. Integration scope: Which sampling algorithms are tested and supported at initial release. |
There was a problem hiding this comment.
I would say it should be all or nothing, we already have too many edge cases in the command line parser up in cmdstan to have more partial coverage like this. Plus, since it follows the writer interface, it should be fine either way
| update_rows_in_header(num_rows_); | ||
| } | ||
|
|
||
| void finalize() { |
There was a problem hiding this comment.
Is there something preventing this from being a destructor?
There was a problem hiding this comment.
From what I understand destructors should not throw errors and given that I/O can fail the finalize makes handling that easier. The real implementation could to some kind of best effort cleanup in a destructor but I think that might be beyond the scope of the spec?
|
Just a quick question re. the process: There are a few things that I would just count as oversights in the proposal that I would simply fix/adapt with a new commit. Is there anything to consider there or do I just push a new commit? |
|
Yep, until the PR is merged it is open for modification via normal commits during the discussions |
|
(one minor note after addressing @WardBrian 's comments): For markdown it's nice to start sentence on a newline so that reviewers can comment on individual sentences easier. Markdown only puts in a true i.e. this will be rendered on the same line The quick brown fox.
Jumps over the lazy dog.but this will be on rendered with a newline The quick brown fox.
Jumps over the lazy dog.You should be able to just do a regex find and replace to replace |
| | Offset | Size | Type | Description | | ||
| |--------|------|------|-------------| | ||
| | 0 | 8 | char[8] | Magic: `"STANBIN\0"` | | ||
| | 8 | 4 | uint32 | Version (`1`) | | ||
| | 12 | 4 | uint32 | Flags (`0` in v1; reserved for extensions) | | ||
| | 16 | 8 | uint64 | Number of rows (draws) | | ||
| | 24 | 8 | uint64 | Number of columns (parameters) | | ||
| | 32 | 4 | uint32 | Data section offset in bytes (`64 + names_size` in v1) | | ||
| | 36 | 4 | uint32 | Names section size in bytes | | ||
| | 40 | 4 | uint32 | Layout parameter (`0` = row-major in v1; non-zero values reserved for extensions such as chunking) | | ||
| | 44 | 8 | uint64 | Metadata section offset (`0` if file not yet finalized) | | ||
| | 52 | 8 | uint64 | Metadata section size in bytes | | ||
| | 60 | 4 | reserved | Reserved for future use | |
There was a problem hiding this comment.
A few minor things here.
- Do we expect the version to need a uint32? We could probably just do
uint8here since idt we will exceed 255 versions - If we are treating flags like a bitset then I think we can remove
Layoutand specify is as a flag bit. I think making the flag uint64 would also be nice since that gives us 64 options to choose from in the future - I think the metadata section size should just be a
uint32. If the size is in bytes idt we will ever go over 4GB of metadata
| | Offset | Size | Type | Description | | |
| |--------|------|------|-------------| | |
| | 0 | 8 | char[8] | Magic: `"STANBIN\0"` | | |
| | 8 | 4 | uint32 | Version (`1`) | | |
| | 12 | 4 | uint32 | Flags (`0` in v1; reserved for extensions) | | |
| | 16 | 8 | uint64 | Number of rows (draws) | | |
| | 24 | 8 | uint64 | Number of columns (parameters) | | |
| | 32 | 4 | uint32 | Data section offset in bytes (`64 + names_size` in v1) | | |
| | 36 | 4 | uint32 | Names section size in bytes | | |
| | 40 | 4 | uint32 | Layout parameter (`0` = row-major in v1; non-zero values reserved for extensions such as chunking) | | |
| | 44 | 8 | uint64 | Metadata section offset (`0` if file not yet finalized) | | |
| | 52 | 8 | uint64 | Metadata section size in bytes | | |
| | 60 | 4 | reserved | Reserved for future use | | |
| | Offset (bytes) | Size (bytes) | Type | Description | | |
| |--------|------|------|-------------| | |
| | 0 | 8 | char[8] | Magic: `"STANBIN\0"` | | |
| | 8 | 8 | uint64 | Flags (see flags below) | | |
| | 16 | 8 | uint64 | Number of rows (draws) | | |
| | 24 | 8 | uint64 | Number of columns (parameters) | | |
| | 32 | 4 | uint32 | Data section offset in bytes (`64 + names_size` in v1) | | |
| | 36 | 4 | uint32 | Names section size in bytes | | |
| | 40 | 8 | uint64 | Metadata section offset (`0` if file not yet finalized) | | |
| | 48 | 4 | uint32 | Metadata section size in bytes | | |
| | 52 | 1 | uint8 | Version (`1`) | | |
| | 53 | 11 | reserved | Reserved for future use | | |
| Flags for each byte | |
| `STAN_CHUNKING_FORMAT`: Specifies data form is in Stan's chunking format | |
| ... |
There was a problem hiding this comment.
I incorporated the metadata-size change to uint32. I left the current version/flags/layout split in place for now because it still reads a bit more directly in the RFC, but I’m happy to revisit that if there is a stronger preference to collapse layout into flags.
| write_row(state); | ||
| ++num_rows_; | ||
| update_rows_in_header(num_rows_); |
There was a problem hiding this comment.
I would add a return flag from write_row so we only update the number of rows / the header if we successfully wrote
| write_row(state); | |
| ++num_rows_; | |
| update_rows_in_header(num_rows_); | |
| const bool write_success = write_row(state); | |
| if (write_success) { | |
| ++num_rows_; | |
| update_rows_in_header(num_rows_); | |
| } |
| write_metadata(); | ||
| rewrite_header(); |
There was a problem hiding this comment.
Same as above I'd have each operation return a flag so you know if writing was successful or not. You could also have an enum that you return for different error codes.
| stream_.write(reinterpret_cast<const char*>(state.data()), | ||
| state.size() * sizeof(double)); |
There was a problem hiding this comment.
| stream_.write(reinterpret_cast<const char*>(state.data()), | |
| state.size() * sizeof(double)); | |
| stream_.write(reinterpret_cast<const std::byte*>(state.data()), | |
| state.size() * sizeof(double)); |
(generally I just like this and think it is more standard now)
|
|
||
| 2. Should stanbin become the default output format in a future CmdStan release, or remain opt-in indefinitely? | ||
|
|
||
| 3. Is the trailing metadata section (raw CSV comment text) the right long-term metadata representation, or should v1 adopt a structured format (e.g., key-value pairs) from the start? |
There was a problem hiding this comment.
imo this is also a good opportunity to have the metadata go to a separate file completly. So we would write a little json file with all the metadata.
|
Hi, I wanted to add a comment on the external metadata issue. I like the idea of having 1 file, but I think external metadata would be much more flexible way of handling it. We could copy idea from zarr world, where they put multiple files in uncompressed zip. I think you can stream data to the zip file and append it too. In the reading side, accessing the metadata would be easy. And this would still keep the file count as 1. |
|
I pushed a revision addressing the straightforward spec fixes from the review. I left a few things out for now as I wasn't sure which way would be the best choice. Happy to adjust things further. How do you prefer handling of resolved comments? Should I resolve them if I think I addressed them or should it be up to the authors? |
No description provided.