Skip to main content

ts_wire/
control.rs

1// Copyright (c) 2026 LightSeek Foundation
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Control operations (`engine/io_struct.py`): cache flush, pause, memory
22//! release, weight updates, profiling and the like.
23//!
24//! A frontend sends one to every engine as a CONTROL message; each engine
25//! answers with the matching reply on its output socket, beside the output
26//! batches. Replies carry no request id: an engine answers each kind of
27//! operation in the order it received them. Every struct is tagged and
28//! starts with the two `BaseReq` fields.
29
30use crate::{
31    codec::{Dict, StrOrList, Wire},
32    error::{Error, Result},
33    msgpack::{Decoder, Encoder},
34    value::Value,
35};
36
37/// Declares tagged control structs that start with the `BaseReq` fields.
38macro_rules! control_struct {
39    ($(
40        $(#[$meta:meta])*
41        $name:ident {
42            $( $(#[$fmeta:meta])* $field:ident: $fty:ty $(= $fdefault:expr)? ),* $(,)?
43        }
44    )*) => {$(
45        wire_struct! {
46            $(#[$meta])*
47            #[derive(Debug, Clone, PartialEq)]
48            pub struct $name: array(stringify!($name)) {
49                /// Request id; control operations rarely set it.
50                pub rid: Option<StrOrList> = None,
51                /// In-process HTTP worker address; unused across processes.
52                pub http_worker_ipc: Option<String> = None,
53                $( $(#[$fmeta])* pub $field: $fty $(= $fdefault)? ),*
54            }
55        }
56    )*};
57}
58
59control_struct! {
60    /// Drop the prefix caches.
61    FlushCacheReqInput {}
62    /// [`FlushCacheReqInput`]'s reply.
63    FlushCacheReqOutput {
64        /// Whether the caches were dropped.
65        success: bool,
66    }
67
68    /// Stop scheduling: `abort` aborts what runs, `wait` lets it finish,
69    /// `keep` freezes it in place.
70    PauseSchedulerReqInput {
71        /// `abort`, `wait` or `keep`.
72        mode: String = "abort".to_owned(),
73    }
74    /// [`PauseSchedulerReqInput`]'s reply, once the pause took hold.
75    PauseSchedulerReqOutput {
76        /// Whether the engine paused.
77        success: bool,
78        /// Why not, if it did not.
79        message: String = String::new(),
80    }
81
82    /// Resume scheduling after a pause.
83    ResumeSchedulerReqInput {}
84    /// [`ResumeSchedulerReqInput`]'s reply.
85    ResumeSchedulerReqOutput {
86        /// Whether the engine resumed.
87        success: bool,
88        /// Why not, if it did not.
89        message: String = String::new(),
90    }
91
92    /// Whether scheduling is paused.
93    IsSchedulerPausedReqInput {}
94    /// [`IsSchedulerPausedReqInput`]'s reply.
95    IsSchedulerPausedReqOutput {
96        /// Paused.
97        is_paused: bool,
98    }
99
100    /// Load new weights from a checkpoint on disk.
101    UpdateWeightFromDiskReqInput {
102        /// The checkpoint.
103        model_path: String,
104        /// Its format.
105        load_format: Option<String> = None,
106        /// The weight version after a successful load.
107        weight_version: Option<String> = None,
108        /// Drop the cached KV first: it was computed under the old weights.
109        flush_cache: bool = true,
110    }
111    /// [`UpdateWeightFromDiskReqInput`]'s reply.
112    UpdateWeightFromDiskReqOutput {
113        /// Whether the weights were loaded.
114        success: bool,
115        /// Detail.
116        message: String,
117        /// Requests paused during the load.
118        num_paused_requests: Option<i64> = Some(0),
119    }
120
121    /// Receive new weights from the trainer's NCCL group.
122    UpdateWeightsFromDistributedReqInput {
123        /// Parameter names, in broadcast order.
124        names: Vec<String>,
125        /// Their dtypes.
126        dtype_names: Vec<String>,
127        /// Their shapes.
128        shapes: Vec<Vec<i64>>,
129        /// The weight-update group.
130        group_name: String = "weight_update_group".to_owned(),
131        /// Drop the cached KV first.
132        flush_cache: bool = true,
133        /// The L3 namespace afterwards; `None` keeps the current one.
134        weight_version: Option<String>,
135    }
136    /// [`UpdateWeightsFromDistributedReqInput`]'s reply.
137    UpdateWeightsFromDistributedReqOutput {
138        /// Whether the weights were received.
139        success: bool,
140        /// Detail.
141        message: String,
142    }
143
144    /// Load weights from serialized tensors; the engines refuse it.
145    UpdateWeightsFromTensorReqInput {
146        /// One serialized tensor dict per world rank.
147        serialized_named_tensors: Vec<Value>,
148        /// Their format.
149        load_format: Option<String>,
150        /// Drop the cached KV first.
151        flush_cache: bool,
152        /// The weight version afterwards.
153        weight_version: Option<String> = None,
154    }
155    /// [`UpdateWeightsFromTensorReqInput`]'s reply.
156    UpdateWeightsFromTensorReqOutput {
157        /// Whether the weights were loaded.
158        success: bool,
159        /// Detail.
160        message: String,
161    }
162
163    /// Join the trainer's weight-update group.
164    InitWeightsUpdateGroupReqInput {
165        /// The group's rendezvous host.
166        master_address: String,
167        /// Its port.
168        master_port: i64,
169        /// This engine's first rank in the group.
170        rank_offset: i64,
171        /// The group's size.
172        world_size: i64,
173        /// The group's name.
174        group_name: String = "weight_update_group".to_owned(),
175        /// Its backend.
176        backend: String = "nccl".to_owned(),
177    }
178    /// [`InitWeightsUpdateGroupReqInput`]'s reply.
179    InitWeightsUpdateGroupReqOutput {
180        /// Whether the engine joined.
181        success: bool,
182        /// Detail.
183        message: String,
184    }
185
186    /// Leave a weight-update group.
187    DestroyWeightsUpdateGroupReqInput {
188        /// The group's name.
189        group_name: String = "weight_update_group".to_owned(),
190    }
191    /// [`DestroyWeightsUpdateGroupReqInput`]'s reply.
192    DestroyWeightsUpdateGroupReqOutput {
193        /// Whether the engine left.
194        success: bool,
195        /// Detail.
196        message: String,
197    }
198
199    /// Load one committed version from a Mooncake weight store.
200    UpdateWeightsFromMooncakeReqInput {
201        /// The version.
202        version: i64,
203        /// Drop the cached KV first.
204        flush_cache: bool,
205        /// The L3 namespace afterwards; `None` for the version's default.
206        weight_version: Option<String>,
207    }
208    /// [`UpdateWeightsFromMooncakeReqInput`]'s reply.
209    UpdateWeightsFromMooncakeReqOutput {
210        /// Whether the weights were loaded.
211        success: bool,
212        /// Detail.
213        message: String,
214    }
215
216    /// Start one online expert rebalance now.
217    RebalanceExpertsReqInput {}
218    /// [`RebalanceExpertsReqInput`]'s reply.
219    RebalanceExpertsReqOutput {
220        /// Whether the rebalance started.
221        success: bool,
222        /// Detail.
223        message: String,
224    }
225
226    /// Release GPU memory (`weights`, `kv_cache`; all when `None`).
227    ReleaseMemoryOccupationReqInput {
228        /// The regions.
229        tags: Option<Vec<String>> = None,
230    }
231    /// [`ReleaseMemoryOccupationReqInput`]'s reply.
232    ReleaseMemoryOccupationReqOutput {
233        /// Whether the memory was released.
234        success: bool = true,
235        /// Detail.
236        message: String = String::new(),
237    }
238
239    /// Take released GPU memory back (all released regions when `None`).
240    ResumeMemoryOccupationReqInput {
241        /// The regions.
242        tags: Option<Vec<String>> = None,
243    }
244    /// [`ResumeMemoryOccupationReqInput`]'s reply.
245    ResumeMemoryOccupationReqOutput {
246        /// Whether the memory was taken back.
247        success: bool = true,
248        /// Detail.
249        message: String = String::new(),
250    }
251
252    /// Whether the engine released its memory.
253    IsSleepingReqInput {}
254    /// [`IsSleepingReqInput`]'s reply.
255    IsSleepingReqOutput {
256        /// Released.
257        is_sleeping: bool,
258    }
259
260    /// The engine's internal state.
261    GetInternalStateReq {}
262    /// [`GetInternalStateReq`]'s reply.
263    GetInternalStateReqOutput {
264        /// The state.
265        internal_state: Dict<Value, Value>,
266    }
267
268    /// Change server arguments at run time.
269    SetInternalStateReq {
270        /// The arguments to change.
271        server_args: Dict<Value, Value>,
272    }
273    /// [`SetInternalStateReq`]'s reply.
274    SetInternalStateReqOutput {
275        /// Whether anything changed.
276        updated: bool,
277        /// The arguments now.
278        server_args: Dict<Value, Value>,
279    }
280
281    /// Start (`kind` 1) or stop (`kind` 2) the profiler.
282    ProfileReq {
283        /// `ProfileReqType`: 1 starts, 2 stops.
284        kind: i64,
285        /// Where traces go.
286        output_dir: Option<String> = None,
287        /// The forward step to start at.
288        start_step: Option<i64> = None,
289        /// Steps to profile before stopping.
290        num_steps: Option<i64> = None,
291        /// What to record (`CPU`, `GPU`, `MEM`, ...).
292        activities: Option<Vec<String>> = None,
293        /// One trace per stage (prefill, decode).
294        profile_by_stage: bool = false,
295        /// Record Python stacks.
296        with_stack: Option<bool> = None,
297        /// Record tensor shapes.
298        record_shapes: Option<bool> = None,
299        /// Names the trace files.
300        profile_id: Option<String> = None,
301    }
302    /// [`ProfileReq`]'s reply.
303    ProfileReqOutput {
304        /// Whether the profiler changed state.
305        success: bool,
306        /// Detail.
307        message: String,
308    }
309}
310
311/// `ProfileReq.kind` that starts the profiler.
312pub const PROFILE_START: i64 = 1;
313/// `ProfileReq.kind` that stops it.
314pub const PROFILE_STOP: i64 = 2;
315
316/// Declares an enum over tagged structs, decoded by their tag.
317macro_rules! tagged_union {
318    (
319        $(#[$meta:meta])*
320        $vis:vis enum $name:ident { $( $(#[$vmeta:meta])* $variant:ident($ty:ident) ),* $(,)? }
321    ) => {
322        $(#[$meta])*
323        $vis enum $name {
324            $( $(#[$vmeta])* $variant($ty) ),*
325        }
326
327        impl $name {
328            /// The variant's wire tag (its Python class name).
329            pub fn tag(&self) -> &'static str {
330                match self {
331                    $( Self::$variant(_) => stringify!($ty) ),*
332                }
333            }
334
335            /// Whether `tag` names a variant.
336            pub fn knows(tag: &str) -> bool {
337                matches!(tag, $( stringify!($ty) )|*)
338            }
339        }
340
341        impl Wire for $name {
342            fn encode(&self, encoder: &mut Encoder) {
343                match self {
344                    $( Self::$variant(value) => value.encode(encoder) ),*
345                }
346            }
347
348            fn decode(decoder: &mut Decoder<'_>) -> Result<Self> {
349                match decoder.peek_tag()? {
350                    $( stringify!($ty) => Ok(Self::$variant($ty::decode(decoder)?)), )*
351                    other => Err(Error::Invalid(format!(
352                        concat!("unknown ", stringify!($name), " {}"),
353                        other
354                    ))),
355                }
356            }
357        }
358    };
359}
360
361tagged_union! {
362    /// A control operation, the payload of a CONTROL message.
363    #[derive(Debug, Clone, PartialEq)]
364    pub enum ControlRequest {
365        /// See [`FlushCacheReqInput`].
366        FlushCache(FlushCacheReqInput),
367        /// See [`PauseSchedulerReqInput`].
368        PauseScheduler(PauseSchedulerReqInput),
369        /// See [`ResumeSchedulerReqInput`].
370        ResumeScheduler(ResumeSchedulerReqInput),
371        /// See [`IsSchedulerPausedReqInput`].
372        IsSchedulerPaused(IsSchedulerPausedReqInput),
373        /// See [`UpdateWeightFromDiskReqInput`].
374        UpdateWeightFromDisk(UpdateWeightFromDiskReqInput),
375        /// See [`UpdateWeightsFromDistributedReqInput`].
376        UpdateWeightsFromDistributed(UpdateWeightsFromDistributedReqInput),
377        /// See [`UpdateWeightsFromTensorReqInput`].
378        UpdateWeightsFromTensor(UpdateWeightsFromTensorReqInput),
379        /// See [`InitWeightsUpdateGroupReqInput`].
380        InitWeightsUpdateGroup(InitWeightsUpdateGroupReqInput),
381        /// See [`DestroyWeightsUpdateGroupReqInput`].
382        DestroyWeightsUpdateGroup(DestroyWeightsUpdateGroupReqInput),
383        /// See [`UpdateWeightsFromMooncakeReqInput`].
384        UpdateWeightsFromMooncake(UpdateWeightsFromMooncakeReqInput),
385        /// See [`RebalanceExpertsReqInput`].
386        RebalanceExperts(RebalanceExpertsReqInput),
387        /// See [`ReleaseMemoryOccupationReqInput`].
388        ReleaseMemoryOccupation(ReleaseMemoryOccupationReqInput),
389        /// See [`ResumeMemoryOccupationReqInput`].
390        ResumeMemoryOccupation(ResumeMemoryOccupationReqInput),
391        /// See [`IsSleepingReqInput`].
392        IsSleeping(IsSleepingReqInput),
393        /// See [`GetInternalStateReq`].
394        GetInternalState(GetInternalStateReq),
395        /// See [`SetInternalStateReq`].
396        SetInternalState(SetInternalStateReq),
397        /// See [`ProfileReq`].
398        Profile(ProfileReq),
399    }
400}
401
402tagged_union! {
403    /// A control operation's reply.
404    #[derive(Debug, Clone, PartialEq)]
405    pub enum ControlOutput {
406        /// See [`FlushCacheReqOutput`].
407        FlushCache(FlushCacheReqOutput),
408        /// See [`PauseSchedulerReqOutput`].
409        PauseScheduler(PauseSchedulerReqOutput),
410        /// See [`ResumeSchedulerReqOutput`].
411        ResumeScheduler(ResumeSchedulerReqOutput),
412        /// See [`IsSchedulerPausedReqOutput`].
413        IsSchedulerPaused(IsSchedulerPausedReqOutput),
414        /// See [`UpdateWeightFromDiskReqOutput`].
415        UpdateWeightFromDisk(UpdateWeightFromDiskReqOutput),
416        /// See [`UpdateWeightsFromDistributedReqOutput`].
417        UpdateWeightsFromDistributed(UpdateWeightsFromDistributedReqOutput),
418        /// See [`UpdateWeightsFromTensorReqOutput`].
419        UpdateWeightsFromTensor(UpdateWeightsFromTensorReqOutput),
420        /// See [`InitWeightsUpdateGroupReqOutput`].
421        InitWeightsUpdateGroup(InitWeightsUpdateGroupReqOutput),
422        /// See [`DestroyWeightsUpdateGroupReqOutput`].
423        DestroyWeightsUpdateGroup(DestroyWeightsUpdateGroupReqOutput),
424        /// See [`UpdateWeightsFromMooncakeReqOutput`].
425        UpdateWeightsFromMooncake(UpdateWeightsFromMooncakeReqOutput),
426        /// See [`RebalanceExpertsReqOutput`].
427        RebalanceExperts(RebalanceExpertsReqOutput),
428        /// See [`ReleaseMemoryOccupationReqOutput`].
429        ReleaseMemoryOccupation(ReleaseMemoryOccupationReqOutput),
430        /// See [`ResumeMemoryOccupationReqOutput`].
431        ResumeMemoryOccupation(ResumeMemoryOccupationReqOutput),
432        /// See [`IsSleepingReqOutput`].
433        IsSleeping(IsSleepingReqOutput),
434        /// See [`GetInternalStateReqOutput`].
435        GetInternalState(GetInternalStateReqOutput),
436        /// See [`SetInternalStateReqOutput`].
437        SetInternalState(SetInternalStateReqOutput),
438        /// See [`ProfileReqOutput`].
439        Profile(ProfileReqOutput),
440    }
441}
442
443impl ControlRequest {
444    /// The tag of the reply this operation gets.
445    pub fn reply_tag(&self) -> &'static str {
446        match self {
447            Self::FlushCache(_) => "FlushCacheReqOutput",
448            Self::PauseScheduler(_) => "PauseSchedulerReqOutput",
449            Self::ResumeScheduler(_) => "ResumeSchedulerReqOutput",
450            Self::IsSchedulerPaused(_) => "IsSchedulerPausedReqOutput",
451            Self::UpdateWeightFromDisk(_) => "UpdateWeightFromDiskReqOutput",
452            Self::UpdateWeightsFromDistributed(_) => "UpdateWeightsFromDistributedReqOutput",
453            Self::UpdateWeightsFromTensor(_) => "UpdateWeightsFromTensorReqOutput",
454            Self::InitWeightsUpdateGroup(_) => "InitWeightsUpdateGroupReqOutput",
455            Self::DestroyWeightsUpdateGroup(_) => "DestroyWeightsUpdateGroupReqOutput",
456            Self::UpdateWeightsFromMooncake(_) => "UpdateWeightsFromMooncakeReqOutput",
457            Self::RebalanceExperts(_) => "RebalanceExpertsReqOutput",
458            Self::ReleaseMemoryOccupation(_) => "ReleaseMemoryOccupationReqOutput",
459            Self::ResumeMemoryOccupation(_) => "ResumeMemoryOccupationReqOutput",
460            Self::IsSleeping(_) => "IsSleepingReqOutput",
461            Self::GetInternalState(_) => "GetInternalStateReqOutput",
462            Self::SetInternalState(_) => "SetInternalStateReqOutput",
463            Self::Profile(_) => "ProfileReqOutput",
464        }
465    }
466}
467
468#[cfg(test)]
469mod tests {
470    use super::*;
471    use crate::codec::{decode_frames, encode_frames};
472
473    #[test]
474    fn a_reply_tag_names_a_reply() {
475        let requests = [
476            ControlRequest::FlushCache(FlushCacheReqInput {
477                rid: None,
478                http_worker_ipc: None,
479            }),
480            ControlRequest::PauseScheduler(PauseSchedulerReqInput {
481                rid: None,
482                http_worker_ipc: None,
483                mode: "wait".to_owned(),
484            }),
485        ];
486        for request in requests {
487            assert!(ControlOutput::knows(request.reply_tag()));
488            assert!(ControlRequest::knows(request.tag()));
489            let decoded: ControlRequest = decode_frames(&encode_frames(&request)).unwrap();
490            assert_eq!(decoded, request);
491        }
492        assert!(!ControlOutput::knows("BatchTokenIDOutSlim"));
493    }
494}