Skip to main content

hurray_io/stream/
writer.rs

1use hurray_core::TensorDescriptor;
2use tokio::io::{AsyncWrite, AsyncWriteExt};
3
4use crate::stream::sans_io::{StreamWriterCore, WriteOp, WriteStep};
5// `pub use`: 0.1.0 published this at `stream::writer::CompositeNode` too (see reader.rs).
6pub use crate::stream::types::CompositeNode;
7use crate::{Error, Result};
8
9/// Writes tensors to an async sink in the Hurray streaming wire format.
10///
11/// The wire format is bare concatenation: each tensor is represented as its
12/// encoded [`TensorDescriptor`] immediately followed by each buffer's raw bytes.
13/// No outer framing, no alignment padding between tensors.
14///
15/// This is a driver, not an encoder: [`StreamWriterCore`] holds the wire format and emits
16/// the bytes, and this type only hands them to the sink (ADR-041 § 7). Payloads are never
17/// assembled or copied on the way — each slice goes to the sink as it is (§ 3).
18///
19/// # Examples
20///
21/// ```no_run
22/// # #[tokio::main]
23/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
24/// use hurray_core::{
25///     BufferHandle, DeviceTag, ElementType, LayoutDescriptor, Shape, SyncMode,
26///     TensorDescriptor, MIN_BUFFER_ALIGNMENT,
27/// };
28/// use hurray_io::stream::StreamWriter;
29///
30/// let handle = BufferHandle::new(64, MIN_BUFFER_ALIGNMENT, DeviceTag::Cpu, SyncMode::ProducerSynced)?;
31/// let shape = Shape::new(vec![4u64, 4]).unwrap();
32/// let desc = TensorDescriptor::new(
33///     1, 0,
34///     ElementType::Float32,
35///     shape,
36///     0,
37///     LayoutDescriptor::RowMajor,
38///     vec![handle],
39///     None, None, None, None,
40/// )?;
41/// let data = vec![0u8; 64];
42///
43/// let mut wire = Vec::<u8>::new();
44/// let mut writer = StreamWriter::new(&mut wire);
45/// writer.write_tensor(&desc, &[&data]).await?;
46/// writer.finish().await?;
47/// # Ok(())
48/// # }
49/// ```
50pub struct StreamWriter<W> {
51    inner: W,
52    core: StreamWriterCore,
53}
54
55impl<W: AsyncWrite + Unpin> StreamWriter<W> {
56    /// Creates a writer that accepts any valid [`SyncMode`][hurray_core::SyncMode].
57    pub fn new(inner: W) -> Self {
58        Self {
59            inner,
60            core: StreamWriterCore::new(),
61        }
62    }
63
64    /// Creates a writer that rejects buffers whose `sync_mode` is not
65    /// [`ProducerSynced`][hurray_core::SyncMode::ProducerSynced].
66    ///
67    /// Use this when the stream crosses machine boundaries where GPU and
68    /// semaphore-based sync primitives are not transferable.
69    pub fn cross_machine(inner: W) -> Self {
70        Self {
71            inner,
72            core: StreamWriterCore::cross_machine(),
73        }
74    }
75
76    /// Encodes and writes one tensor.
77    ///
78    /// # Errors
79    ///
80    /// - [`Error::MultiBufferLengthMismatch`] — `buffers.len()` ≠ `desc.buffers.len()`
81    /// - [`Error::BufferSizeMismatch`] — a buffer's byte length ≠ its handle's `byte_size`
82    /// - [`Error::InvalidCrossMachineSyncMode`] — cross-machine mode and a buffer has a
83    ///   non-`ProducerSynced` sync mode
84    /// - [`Error::SinkClosed`] — the sink accepted zero bytes mid-item
85    /// - [`Error::CoreUnusable`] — an earlier write left this writer's sink poisoned
86    ///
87    /// A failure *before the first byte of this tensor* leaves the writer usable, so the
88    /// same tensor may be written again; a failure after it poisons the writer, because a
89    /// truncated item is on the wire and a stream has no resynchronisation point (§ 6b).
90    /// - [`Error::Core`] — descriptor encoding failed
91    /// - [`Error::Io`] — underlying write error
92    pub async fn write_tensor(&mut self, desc: &TensorDescriptor, buffers: &[&[u8]]) -> Result<()> {
93        let mut op = self.core.tensor(desc, buffers)?;
94        drain(&mut op, &mut self.inner).await
95    }
96
97    /// Encodes and writes a composite tensor: its head followed by every member's
98    /// descriptor and data, in order (ADR-027 § Binding).
99    ///
100    /// The group is validated *before* any byte is written — reusing
101    /// [`CompositeValidator`][hurray_core::CompositeValidator] to check `member_count` and
102    /// the per-rule constraints (partition exact-cover / non-overlap, overlay base-first
103    /// ordering) — so a torn or invalid composite never reaches the wire. Members that are
104    /// themselves composites are written as part of the same operation (head precedes its
105    /// members at every level), preserving the forward, self-delimiting,
106    /// back-reference-free wire contract.
107    ///
108    /// # Examples
109    ///
110    /// ```no_run
111    /// # #[tokio::main]
112    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
113    /// use hurray_core::{
114    ///     layout::{CompositeLayout, CompositionRule, LayoutDescriptor},
115    ///     ElementType, Shape, ShardDescriptor, TensorDescriptor,
116    /// };
117    /// use hurray_io::stream::{CompositeNode, StreamWriter};
118    ///
119    /// // A partition head [8, 8] split into two [8, 4] members (buffers elided here).
120    /// let head = TensorDescriptor::new(
121    ///     1, 0, ElementType::Float32, Shape::new(vec![8u64, 8]).unwrap(), 0,
122    ///     LayoutDescriptor::Composite(CompositeLayout::new(CompositionRule::Partition, 2).unwrap()),
123    ///     vec![], None, None, None, None,
124    /// )?;
125    /// # let member_descs: Vec<TensorDescriptor> = vec![];
126    /// # let members: Vec<CompositeNode> = vec![];
127    /// let mut wire = Vec::<u8>::new();
128    /// let mut writer = StreamWriter::new(&mut wire);
129    /// writer.write_composite(&head, &members).await?;
130    /// writer.finish().await?;
131    /// # Ok(())
132    /// # }
133    /// ```
134    ///
135    /// # Errors
136    ///
137    /// - [`Error::Core`] — the head is not a valid composite head, or validation failed
138    ///   (member-count mismatch, partition coverage, overlay ordering)
139    /// - the same buffer/sync/sink errors as [`write_tensor`][StreamWriter::write_tensor]
140    ///   for each member
141    /// - [`Error::Io`] — underlying write error
142    pub async fn write_composite(
143        &mut self,
144        head: &TensorDescriptor,
145        members: &[CompositeNode<'_>],
146    ) -> Result<()> {
147        let mut op = self.core.composite(head, members)?;
148        drain(&mut op, &mut self.inner).await
149    }
150
151    /// Flushes the underlying sink and returns it.
152    pub async fn finish(mut self) -> Result<W> {
153        self.inner.flush().await?;
154        Ok(self.inner)
155    }
156}
157
158/// Writes every byte of one submitted item to `sink`.
159///
160/// A free function rather than a method: the operation holds a mutable borrow of the
161/// writer's core for as long as it lives, so only disjoint *field* borrows let the sink be
162/// used at the same time.
163async fn drain<W: AsyncWrite + Unpin>(op: &mut WriteOp<'_>, sink: &mut W) -> Result<()> {
164    while let WriteStep::Output { .. } = op.step()? {
165        let n = {
166            let src = op.output_src();
167            loop {
168                match sink.write(src).await {
169                    Ok(n) => break n,
170                    // `write_all`, which this replaces, absorbed this internally.
171                    Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
172                    // This call put no bytes on the wire, so its count is known to be
173                    // zero. That is not the same as the item being intact: if an earlier
174                    // call in this item moved bytes, the core is already torn and the next
175                    // call on this writer reports `CoreUnusable` (§ 6b).
176                    Err(e) => return Err(Error::Io(e)),
177                }
178            }
179        };
180
181        if n == 0 {
182            // § 4: a zero-length write is never progress. A closed sink is its own signal,
183            // not an I/O error to retry.
184            return Err(op.report_sink_closed());
185        }
186        op.report_written(n)?;
187    }
188    Ok(())
189}