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}