Skip to main content

cubecl_runtime/stream/
event.rs

1use crate::{
2    config::streaming::StreamingLogLevel,
3    logging::ServerLogger,
4    memory_management::{ManagedMemoryId, SharedMemoryBindings},
5    server::{Binding, ServerError},
6    stream::{StreamFactory, StreamPool},
7};
8use core::any::Any;
9use cubecl_environment::backtrace::BackTrace;
10use cubecl_environment::collections::HashMap;
11use cubecl_environment::stream::StreamId;
12use std::{
13    boxed::Box,
14    format,
15    sync::{Arc, mpsc::SyncSender},
16    vec::Vec,
17};
18
19/// Trait defining the backend operations for managing streams and events.
20///
21/// This trait provides the necessary methods for initializing streams, flushing them to create events,
22/// and waiting on events for synchronization purposes.
23pub trait EventStreamBackend: 'static {
24    /// The type representing a stream in this backend.
25    type Stream: core::fmt::Debug;
26    /// The type representing an event in this backend.
27    type Event: Send + 'static;
28
29    /// Initializes and returns a new stream associated with the given stream ID.
30    fn create_stream(&self) -> Self::Stream;
31    /// Returns the cursor of the given handle on the given stream.
32    fn handle_cursor(stream: &Self::Stream, handle: &Binding) -> u64;
33    /// Returns whether the stream can access new tasks.
34    fn is_healthy(stream: &Self::Stream) -> bool;
35
36    /// Flushes the given stream, ensuring all pending operations are submitted, and returns an event
37    /// that can be used for synchronization.
38    fn flush(stream: &mut Self::Stream) -> Self::Event;
39    /// Makes the stream wait for the specified event to complete before proceeding with further operations.
40    fn wait_event(stream: &mut Self::Stream, event: Self::Event);
41    /// Wait for the given event synching the CPU.
42    fn wait_event_sync(event: Self::Event) -> Result<(), ServerError>;
43}
44
45/// Manages multiple streams with synchronization logic based on shared bindings.
46///
47/// This struct handles the creation and alignment of streams to ensure proper synchronization
48/// when bindings (e.g., buffers) are shared across different streams.
49#[derive(Debug)]
50pub struct MultiStream<B: EventStreamBackend> {
51    /// The map of stream IDs to their corresponding stream wrappers.
52    streams: StreamPool<EventStreamBackendWrapper<B>>,
53    /// The logger used by the server.
54    pub logger: Arc<ServerLogger>,
55    max_streams: usize,
56    gc: GcThread<B>,
57    shared_bindings_pool: Vec<(ManagedMemoryId, StreamId, u64)>,
58}
59
60/// A wrapper around a backend stream that includes synchronization metadata.
61///
62/// This includes the stream itself, a map of last synchronized cursors from other streams,
63/// and the current cursor position for this stream.
64pub(crate) struct StreamWrapper<B: EventStreamBackend> {
65    /// The underlying backend stream.
66    stream: B::Stream,
67    /// The current cursor position, representing the logical progress or version of operations on this stream.
68    cursor: u64,
69    /// A map tracking the last synchronized cursor positions from other streams.
70    last_synced: HashMap<usize, u64>,
71}
72
73/// Streams that are synchronized correctly after a [`MultiStream::resolve`] is called.
74pub struct ResolvedStreams<'a, B: EventStreamBackend> {
75    /// The cursor on the current stream.
76    ///
77    /// This cursor should be use for new allocations happening on the current stream.
78    pub cursor: u64,
79    streams: &'a mut StreamPool<EventStreamBackendWrapper<B>>,
80    analysis: SharedBindingAnalysis,
81    gc: &'a GcThread<B>,
82    /// The current stream where new tasks can be sent safely.
83    pub current: StreamId,
84}
85
86#[derive(Debug)]
87/// A task to be enqueue on the gc stream that will be clearned after an event is reached.
88pub struct GcTask<B: EventStreamBackend> {
89    to_drop: Box<dyn Any + Send + 'static>,
90    /// The event to sync making sure the bindings in the batch are ready to be reused by other streams.
91    event: B::Event,
92}
93
94impl<B: EventStreamBackend> GcTask<B> {
95    /// Creates a new task that will be clearned when the event is reached.
96    pub fn new<T: Send + 'static>(to_drop: T, event: B::Event) -> Self {
97        Self {
98            to_drop: Box::new(to_drop),
99            event,
100        }
101    }
102}
103
104#[derive(Debug)]
105struct EventStreamBackendWrapper<B: EventStreamBackend> {
106    backend: B,
107}
108
109impl<B: EventStreamBackend> StreamFactory for EventStreamBackendWrapper<B> {
110    type Stream = StreamWrapper<B>;
111
112    fn create(&mut self) -> Self::Stream {
113        StreamWrapper {
114            stream: self.backend.create_stream(),
115            cursor: 0,
116            last_synced: Default::default(),
117        }
118    }
119}
120
121#[derive(Debug)]
122struct GcThread<B: EventStreamBackend> {
123    sender: SyncSender<GcTask<B>>,
124}
125
126impl<B: EventStreamBackend> GcThread<B> {
127    fn new() -> GcThread<B> {
128        let (sender, recv) = std::sync::mpsc::sync_channel::<GcTask<B>>(8);
129
130        cubecl_environment::thread::spawn(move || {
131            while let Ok(event) = recv.recv() {
132                B::wait_event_sync(event.event).unwrap();
133                core::mem::drop(event.to_drop);
134            }
135        });
136
137        GcThread { sender }
138    }
139    fn register(&self, task: GcTask<B>) {
140        self.sender.send(task).unwrap()
141    }
142}
143
144fn stream_index(stream_id: &StreamId, max_streams: usize) -> usize {
145    stream_id.value as usize % max_streams
146}
147
148impl<'a, B: EventStreamBackend> ResolvedStreams<'a, B> {
149    /// Get the stream associated to the given [`stream_id`](StreamId).
150    pub fn get(&mut self, stream_id: &StreamId) -> &mut B::Stream {
151        let stream = self.streams.get_mut(stream_id);
152        &mut stream.stream
153    }
154
155    /// Get the stream associated to the [current `stream_id`](StreamId).
156    pub fn current(&mut self) -> &mut B::Stream {
157        let stream = self.streams.get_mut(&self.current);
158        &mut stream.stream
159    }
160
161    /// Enqueue a task to be cleaned.
162    pub fn gc(&mut self, gc: GcTask<B>) {
163        self.gc.sender.send(gc).unwrap();
164    }
165}
166
167impl<'a, B: EventStreamBackend> Drop for ResolvedStreams<'a, B> {
168    fn drop(&mut self) {
169        if self.analysis.pinned.is_empty() {
170            return;
171        }
172
173        let stream = self.streams.get_mut(&self.current);
174        let event_origin = B::flush(&mut stream.stream);
175
176        let stream_gc = &mut unsafe { self.streams.get_special(0) }.stream;
177        B::wait_event(stream_gc, event_origin);
178        let event = B::flush(stream_gc);
179
180        let pinned = core::mem::take(&mut self.analysis.pinned);
181        self.gc.register(GcTask::new(pinned, event));
182    }
183}
184
185impl<B: EventStreamBackend> MultiStream<B> {
186    /// Mutable access to the stream-creation backend, e.g. to change the
187    /// configuration new streams are created with. Already-created streams are
188    /// unaffected.
189    pub fn backend_mut(&mut self) -> &mut B {
190        &mut self.streams.factory_mut().backend
191    }
192
193    /// Creates an empty multi-stream.
194    pub fn new(logger: Arc<ServerLogger>, backend: B, max_streams: u8) -> Self {
195        let wrapper = EventStreamBackendWrapper { backend };
196        Self {
197            streams: StreamPool::new(wrapper, max_streams, 1),
198            logger,
199            max_streams: max_streams as usize,
200            gc: GcThread::new(),
201            shared_bindings_pool: Vec::new(),
202        }
203    }
204
205    /// Synthetic [`StreamId`]s, one per initialized stream (see [`StreamPool::stream_ids`]).
206    pub fn stream_ids(&self) -> impl Iterator<Item = StreamId> + '_ {
207        self.streams.stream_ids()
208    }
209
210    /// Enqueue a task to be cleaned.
211    pub fn gc(&mut self, gc: GcTask<B>) {
212        self.gc.sender.send(gc).unwrap();
213    }
214
215    /// Resolves and returns a mutable reference to the stream for the given ID, performing any necessary
216    /// alignment based on the provided bindings.
217    ///
218    /// This method ensures that the stream is synchronized with any shared bindings from other streams
219    /// before returning the stream reference.
220    pub fn resolve<'a>(
221        &mut self,
222        stream_id: StreamId,
223        handles: impl Iterator<Item = &'a Binding>,
224        enforce_healthy: bool,
225    ) -> Result<ResolvedStreams<'_, B>, ServerError> {
226        let analysis = self.align_streams(stream_id, handles);
227
228        let stream = self.streams.get_mut(&stream_id);
229        stream.cursor += 1;
230
231        if enforce_healthy && !B::is_healthy(&stream.stream) {
232            return Err(ServerError::Generic {
233                reason: "Can't resolve the stream since it is currently in an error state".into(),
234                backtrace: BackTrace::capture(),
235            });
236        }
237
238        Ok(ResolvedStreams {
239            cursor: stream.cursor,
240            streams: &mut self.streams,
241            current: stream_id,
242            analysis,
243            gc: &self.gc,
244        })
245    }
246
247    /// Aligns the target stream with other streams based on shared bindings.
248    ///
249    /// This initializes the stream if it doesn't exist, analyzes which originating streams need flushing
250    /// for synchronization, flushes them, and waits on the events in the target stream.
251    fn align_streams<'a>(
252        &mut self,
253        stream_id: StreamId,
254        handles: impl Iterator<Item = &'a Binding>,
255    ) -> SharedBindingAnalysis {
256        let analysis = self.update_shared_bindings(stream_id, handles);
257
258        self.apply_analysis(stream_id, analysis)
259    }
260
261    /// Updates and analyzes the bindings to determine which streams need alignment (flushing and waiting).
262    ///
263    /// This checks for shared bindings from other streams and determines if synchronization is needed
264    /// based on cursor positions.
265    pub(crate) fn update_shared_bindings<'a>(
266        &mut self,
267        stream_id: StreamId,
268        handles: impl Iterator<Item = &'a Binding>,
269    ) -> SharedBindingAnalysis {
270        // We reset the memory pool for the info.
271        self.shared_bindings_pool.clear();
272
273        let mut analysis = SharedBindingAnalysis::default();
274
275        // We only consider handles whose stream is different from the current stream.
276        for handle in handles.filter(|handle| handle.stream != stream_id) {
277            let index = stream_index(&handle.stream, self.max_streams);
278            let stream = unsafe { self.streams.get_mut_index(index) };
279            let cursor_handle = B::handle_cursor(&stream.stream, handle);
280
281            self.shared_bindings_pool.push((
282                handle.memory.descriptor().id,
283                handle.stream,
284                cursor_handle,
285            ));
286            // Pinned unconditionally, even when the cursor check below decides
287            // no new wait is needed: the reverse-direction hazard (the origin
288            // stream freeing/reusing the memory under the in-flight consumer)
289            // exists either way.
290            analysis.pinned.push(handle.memory.clone());
291        }
292
293        let current = self.streams.get_mut(&stream_id);
294
295        for (handle_id, stream, cursor) in self.shared_bindings_pool.iter() {
296            let index = stream_index(stream, self.max_streams);
297
298            if let Some(last_synced) = current.last_synced.get(&index) {
299                if last_synced < cursor {
300                    self.logger.log_streaming(
301                        |level| matches!(level, StreamingLogLevel::Full),
302                        || {
303                            format!(
304                                "Binding on {} is shared on {} since it's not sync {} < {}",
305                                stream, stream_id, last_synced, cursor
306                            )
307                        },
308                    );
309                    analysis.shared(*handle_id, index);
310                }
311            } else {
312                self.logger.log_streaming(
313                    |level| matches!(level, StreamingLogLevel::Full),
314                    || {
315                        format!(
316                            "Binding on {} is shared on {} since it was never synced.",
317                            stream, stream_id,
318                        )
319                    },
320                );
321                analysis.shared(*handle_id, index);
322            }
323        }
324
325        analysis
326    }
327
328    pub(crate) fn apply_analysis(
329        &mut self,
330        stream_id: StreamId,
331        analysis: SharedBindingAnalysis,
332    ) -> SharedBindingAnalysis {
333        if analysis.slices.is_empty() {
334            return analysis;
335        }
336
337        let mut events = Vec::with_capacity(analysis.slices.len());
338
339        unsafe {
340            for origin in analysis.slices.keys() {
341                let stream = self.streams.get_mut_index(*origin);
342                let event = B::flush(&mut stream.stream);
343
344                events.push(((origin, stream.cursor), event));
345            }
346        }
347
348        let stream = self.streams.get_mut(&stream_id);
349
350        for ((stream_origin, cursor_origin), event) in events {
351            stream.last_synced.insert(*stream_origin, cursor_origin);
352
353            self.logger.log_streaming(
354                |level| !matches!(level, StreamingLogLevel::Disabled),
355                || format!("Waiting on {stream_origin} from {stream_id}",),
356            );
357
358            B::wait_event(&mut stream.stream, event);
359        }
360
361        analysis
362    }
363}
364
365impl<B: EventStreamBackend> core::fmt::Debug for StreamWrapper<B> {
366    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
367        f.debug_struct("StreamWrapper")
368            .field("stream", &self.stream)
369            .field("cursor", &self.cursor)
370            .field("last_synced", &self.last_synced)
371            .finish()
372    }
373}
374
375#[derive(Default, Debug)]
376pub(crate) struct SharedBindingAnalysis {
377    slices: HashMap<usize, Vec<ManagedMemoryId>>,
378    /// Every cross-stream binding of the task, kept alive until the consumer
379    /// stream's work completes (released by the GC thread after its event).
380    ///
381    /// The origin stream's pools consider a slice free once no handle/binding
382    /// references its descriptor, but the consumer's kernel may still be
383    /// running on the GPU after the CPU-side bindings were dropped at enqueue
384    /// time. Pinning the bindings here is what keeps the slice non-free until
385    /// the GC event fires, so `cleanup`/`try_reserve` on the origin stream
386    /// cannot dealloc or reuse memory that is still read by another stream.
387    pinned: SharedMemoryBindings,
388}
389
390/// Equality covers the sync analysis only; `pinned` is a lifetime mechanism,
391/// not part of the analysis result.
392impl PartialEq for SharedBindingAnalysis {
393    fn eq(&self, other: &Self) -> bool {
394        self.slices == other.slices
395    }
396}
397
398impl Eq for SharedBindingAnalysis {}
399
400impl SharedBindingAnalysis {
401    fn shared(&mut self, id: ManagedMemoryId, index: usize) {
402        match self.slices.get_mut(&index) {
403            Some(bindings) => bindings.push(id),
404            None => {
405                self.slices.insert(index, alloc::vec![id]);
406            }
407        }
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use crate::server::Handle;
414    use core::sync::atomic::{AtomicBool, Ordering};
415
416    use super::*;
417
418    const MAX_STREAMS: u8 = 4;
419
420    #[test_log::test]
421    fn test_analysis_shared_bindings_1() {
422        let logger = Arc::new(ServerLogger::default());
423        let stream_1 = StreamId { value: 1 };
424        let stream_2 = StreamId { value: 2 };
425
426        let binding_1 = handle(stream_1);
427        let binding_2 = handle(stream_2);
428
429        let mut ms = MultiStream::new(logger, TestBackend, MAX_STREAMS);
430        ms.resolve(stream_1, [].into_iter(), false).unwrap();
431        ms.resolve(stream_2, [].into_iter(), false).unwrap();
432
433        let analysis = ms.update_shared_bindings(stream_1, [&binding_1, &binding_2].into_iter());
434
435        let mut expected = SharedBindingAnalysis::default();
436        expected.shared(
437            binding_2.memory.descriptor().id,
438            ms.streams.stream_index(&binding_2.stream),
439        );
440
441        assert_eq!(analysis, expected);
442    }
443
444    #[test_log::test]
445    fn test_analysis_shared_bindings_2() {
446        let logger = Arc::new(ServerLogger::default());
447        let stream_1 = StreamId { value: 1 };
448        let stream_2 = StreamId { value: 2 };
449
450        let binding_1 = handle(stream_1);
451        let binding_2 = handle(stream_2);
452        let binding_3 = handle(stream_1);
453
454        let mut ms = MultiStream::new(logger, TestBackend, 4);
455        ms.resolve(stream_1, [].into_iter(), false).unwrap();
456        ms.resolve(stream_2, [].into_iter(), false).unwrap();
457
458        let analysis =
459            ms.update_shared_bindings(stream_1, [&binding_1, &binding_2, &binding_3].into_iter());
460
461        let mut expected = SharedBindingAnalysis::default();
462        expected.shared(
463            binding_2.memory.descriptor().id,
464            ms.streams.stream_index(&binding_2.stream),
465        );
466
467        assert_eq!(analysis, expected);
468    }
469
470    #[test_log::test]
471    fn test_analysis_no_shared() {
472        let logger = Arc::new(ServerLogger::default());
473        let stream_1 = StreamId { value: 1 };
474        let stream_2 = StreamId { value: 2 };
475
476        let binding_1 = handle(stream_1);
477        let binding_2 = handle(stream_1);
478        let binding_3 = handle(stream_1);
479
480        let mut ms = MultiStream::new(logger, TestBackend, MAX_STREAMS);
481        ms.resolve(stream_1, [].into_iter(), false).unwrap();
482        ms.resolve(stream_2, [].into_iter(), false).unwrap();
483
484        let analysis =
485            ms.update_shared_bindings(stream_1, [&binding_1, &binding_2, &binding_3].into_iter());
486
487        let expected = SharedBindingAnalysis::default();
488
489        assert_eq!(analysis, expected);
490    }
491
492    #[test_log::test]
493    fn test_state() {
494        let logger = Arc::new(ServerLogger::default());
495        let stream_1 = StreamId { value: 1 };
496        let stream_2 = StreamId { value: 2 };
497
498        let binding_1 = handle(stream_1);
499        let binding_2 = handle(stream_2);
500        let binding_3 = handle(stream_1);
501
502        let mut ms = MultiStream::new(logger, TestBackend, MAX_STREAMS);
503        ms.resolve(stream_1, [].into_iter(), false).unwrap();
504        ms.resolve(stream_2, [].into_iter(), false).unwrap();
505
506        ms.resolve(
507            stream_1,
508            [&binding_1, &binding_2, &binding_3].into_iter(),
509            false,
510        )
511        .unwrap();
512
513        let stream1 = ms.streams.get_mut(&stream_1);
514        let index_2 = stream_index(&stream_2, MAX_STREAMS as usize);
515        assert_eq!(stream1.last_synced.get(&index_2), Some(&1));
516        assert_eq!(stream1.cursor, 2);
517
518        let stream2 = ms.streams.get_mut(&stream_2);
519        assert!(stream2.last_synced.is_empty());
520        assert_eq!(stream2.cursor, 1);
521    }
522
523    #[test_log::test]
524    fn test_cross_stream_binding_pinned_until_gc_event() {
525        let logger = Arc::new(ServerLogger::default());
526        let stream_1 = StreamId { value: 1 };
527        let stream_2 = StreamId { value: 2 };
528
529        let gate = Arc::new(AtomicBool::new(false));
530        let mut ms = MultiStream::new(logger, GatedBackend { gate: gate.clone() }, MAX_STREAMS);
531        ms.resolve(stream_1, [].into_iter(), false).unwrap();
532        ms.resolve(stream_2, [].into_iter(), false).unwrap();
533
534        let handle = Handle::new(stream_1, 10);
535        let observer = handle.memory.clone();
536        let binding = handle.binding();
537
538        drop(ms.resolve(stream_2, [&binding].into_iter(), false).unwrap());
539        drop(binding);
540
541        // The GC thread is blocked on the (gated) consumer event, so the pinned
542        // binding must keep the memory non-free even though every user-side
543        // handle/binding is gone.
544        assert!(
545            !observer.is_free(),
546            "cross-stream binding must stay pinned while the consumer event is pending"
547        );
548
549        gate.store(true, Ordering::Release);
550        wait_until_free(&observer);
551    }
552
553    #[test_log::test]
554    fn test_already_synced_cross_stream_binding_still_pinned() {
555        let logger = Arc::new(ServerLogger::default());
556        let stream_1 = StreamId { value: 1 };
557        let stream_2 = StreamId { value: 2 };
558
559        let gate = Arc::new(AtomicBool::new(true));
560        let mut ms = MultiStream::new(logger, GatedBackend { gate: gate.clone() }, MAX_STREAMS);
561        ms.resolve(stream_1, [].into_iter(), false).unwrap();
562        ms.resolve(stream_2, [].into_iter(), false).unwrap();
563
564        // First resolve records stream_1 as synced on stream_2.
565        let handle_1 = Handle::new(stream_1, 10);
566        let binding_1 = handle_1.binding();
567        drop(
568            ms.resolve(stream_2, [&binding_1].into_iter(), false)
569                .unwrap(),
570        );
571        drop(binding_1);
572
573        // Close the gate for the second round.
574        gate.store(false, Ordering::Release);
575
576        let handle_2 = Handle::new(stream_1, 10);
577        let observer = handle_2.memory.clone();
578        let binding_2 = handle_2.binding();
579
580        // stream_2 already synced past this binding's cursor, so the sync
581        // analysis is empty — but the binding must still be pinned: the origin
582        // stream could otherwise free/reuse the memory under the in-flight
583        // consumer.
584        let resolved = ms
585            .resolve(stream_2, [&binding_2].into_iter(), false)
586            .unwrap();
587        assert!(resolved.analysis.slices.is_empty());
588        drop(resolved);
589        drop(binding_2);
590
591        assert!(
592            !observer.is_free(),
593            "already-synced cross-stream binding must still be pinned"
594        );
595
596        gate.store(true, Ordering::Release);
597        wait_until_free(&observer);
598    }
599
600    fn wait_until_free(observer: &crate::memory_management::ManagedMemoryHandle) {
601        let start = std::time::Instant::now();
602        while !observer.is_free() {
603            assert!(
604                start.elapsed() < std::time::Duration::from_secs(10),
605                "pinned binding was never released"
606            );
607            std::thread::yield_now();
608        }
609    }
610
611    fn handle(stream: StreamId) -> Binding {
612        Handle::new(stream, 10).binding()
613    }
614
615    struct TestBackend;
616
617    #[derive(Debug)]
618    struct TestStream {}
619
620    #[derive(Debug)]
621    struct TestEvent {}
622
623    /// A backend whose events complete only once the shared `gate` opens,
624    /// emulating GPU work still in flight on the consumer stream.
625    struct GatedBackend {
626        gate: Arc<AtomicBool>,
627    }
628
629    #[derive(Debug)]
630    struct GatedStream {
631        gate: Arc<AtomicBool>,
632    }
633
634    #[derive(Debug)]
635    struct GatedEvent {
636        gate: Arc<AtomicBool>,
637    }
638
639    impl EventStreamBackend for GatedBackend {
640        type Stream = GatedStream;
641        type Event = GatedEvent;
642
643        fn create_stream(&self) -> Self::Stream {
644            GatedStream {
645                gate: self.gate.clone(),
646            }
647        }
648
649        fn flush(stream: &mut Self::Stream) -> Self::Event {
650            GatedEvent {
651                gate: stream.gate.clone(),
652            }
653        }
654
655        fn wait_event(_stream: &mut Self::Stream, _event: Self::Event) {}
656
657        fn wait_event_sync(event: Self::Event) -> Result<(), ServerError> {
658            while !event.gate.load(Ordering::Acquire) {
659                std::thread::yield_now();
660            }
661            Ok(())
662        }
663
664        fn handle_cursor(_stream: &Self::Stream, _handle: &Binding) -> u64 {
665            0
666        }
667
668        fn is_healthy(_stream: &Self::Stream) -> bool {
669            true
670        }
671    }
672
673    impl EventStreamBackend for TestBackend {
674        type Stream = TestStream;
675        type Event = TestEvent;
676
677        fn create_stream(&self) -> Self::Stream {
678            TestStream {}
679        }
680
681        fn flush(_stream: &mut Self::Stream) -> Self::Event {
682            TestEvent {}
683        }
684
685        fn wait_event(_stream: &mut Self::Stream, _event: Self::Event) {}
686
687        fn wait_event_sync(_event: Self::Event) -> Result<(), ServerError> {
688            Ok(())
689        }
690
691        fn handle_cursor(_stream: &Self::Stream, _handle: &Binding) -> u64 {
692            0
693        }
694
695        fn is_healthy(_stream: &Self::Stream) -> bool {
696            true
697        }
698    }
699}