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}