Skip to main content

cubecl_runtime/logging/
server.rs

1use core::fmt::Display;
2
3use crate::config::memory::MemoryLogLevel;
4use crate::config::streaming::StreamingLogLevel;
5use crate::config::{Logger, compilation::CompilationLogLevel, profiling::ProfilingLogLevel};
6use alloc::format;
7use alloc::string::String;
8use alloc::string::ToString;
9use cubecl_common::profile::ProfileDuration;
10use cubecl_environment::future::channel::{Receiver, Sender};
11use cubecl_environment::future::spawn_detached;
12
13use super::{ProfileLevel, Profiled};
14
15enum LogMessage {
16    Execution(String),
17    Compilation(String),
18    Streaming(String),
19    Memory(String),
20    Profile(String, ProfileDuration),
21    ProfileSummary,
22}
23
24/// Server logger.
25#[derive(Debug)]
26pub struct ServerLogger {
27    profile_level: Option<ProfileLevel>,
28    log_compile_info: bool,
29    log_compile_source: bool,
30    log_streaming: StreamingLogLevel,
31    log_channel: Option<Sender<LogMessage>>,
32    log_memory: MemoryLogLevel,
33}
34
35impl Default for ServerLogger {
36    fn default() -> Self {
37        let logger = Logger::new();
38
39        let disabled = matches!(
40            logger.config.compilation.logger.level,
41            CompilationLogLevel::Disabled
42        ) && matches!(
43            logger.config.profiling.logger.level,
44            ProfilingLogLevel::Disabled
45        ) && matches!(logger.config.memory.logger.level, MemoryLogLevel::Disabled)
46            && matches!(
47                logger.config.streaming.logger.level,
48                StreamingLogLevel::Disabled
49            );
50
51        if disabled {
52            return Self {
53                profile_level: None,
54                log_compile_info: false,
55                log_compile_source: false,
56                log_streaming: StreamingLogLevel::Disabled,
57                log_channel: None,
58                log_memory: MemoryLogLevel::Disabled,
59            };
60        }
61        let profile_level = match logger.config.profiling.logger.level {
62            ProfilingLogLevel::Disabled => None,
63            ProfilingLogLevel::Minimal => Some(ProfileLevel::ExecutionOnly),
64            ProfilingLogLevel::Basic => Some(ProfileLevel::Basic),
65            ProfilingLogLevel::Medium => Some(ProfileLevel::Medium),
66            ProfilingLogLevel::Full => Some(ProfileLevel::Full),
67        };
68
69        let log_compile_info = match logger.config.compilation.logger.level {
70            CompilationLogLevel::Disabled => false,
71            CompilationLogLevel::Basic => true,
72            CompilationLogLevel::Full => true,
73        };
74        let log_compile_source = matches!(
75            logger.config.compilation.logger.level,
76            CompilationLogLevel::Full
77        );
78        let log_streaming = logger.config.streaming.logger.level;
79        let log_memory = logger.config.memory.logger.level;
80
81        let (send, rec) = cubecl_environment::future::channel::unbounded();
82
83        // Spawn the logger as a detached task.
84        let async_logger = AsyncLogger {
85            message: rec,
86            logger,
87            profiled: Default::default(),
88        };
89        // Spawn the future in the background to logs messages / durations.
90        spawn_detached(async_logger.process());
91
92        Self {
93            profile_level,
94            log_compile_info,
95            log_compile_source,
96            log_streaming,
97            log_memory,
98            log_channel: Some(send),
99        }
100    }
101}
102
103impl ServerLogger {
104    /// Returns the profile level, none if profiling is deactivated.
105    pub fn profile_level(&self) -> Option<ProfileLevel> {
106        self.profile_level
107    }
108
109    /// Returns true if compilation info should be logged.
110    pub fn compilation_activated(&self) -> bool {
111        self.log_compile_info
112    }
113
114    /// Returns true if compilation logging includes kernel sources
115    /// ([`CompilationLogLevel::Full`]) — the only level where attaching debug
116    /// info and formatting the source pays off. `Basic` prints a name-only
117    /// line, so runtimes must not spend a `clang-format` subprocess per fresh
118    /// kernel on it.
119    pub fn compilation_source_activated(&self) -> bool {
120        self.log_compile_source
121    }
122
123    /// Log the argument to a file when the compilation logger is activated.
124    pub fn log_compilation<I>(&self, arg: &I)
125    where
126        I: Display,
127    {
128        if let Some(channel) = &self.log_channel
129            && self.log_compile_info
130        {
131            // Channel will never be full, don't care if it's closed.
132            let _ = channel.try_send(LogMessage::Compilation(arg.to_string()));
133        }
134    }
135
136    /// Log the argument to the logger when the streaming logger is activated.
137    pub fn log_streaming<I: FnOnce() -> String, C: FnOnce(StreamingLogLevel) -> bool>(
138        &self,
139        cond: C,
140        format: I,
141    ) {
142        if let Some(channel) = &self.log_channel
143            && cond(self.log_streaming)
144        {
145            // Channel will never be full, don't care if it's closed.
146            let _ = channel.try_send(LogMessage::Streaming(format()));
147        }
148    }
149
150    /// Log the argument to the logger when the memory logger is activated.
151    pub fn log_memory<I: FnOnce() -> String, C: FnOnce(MemoryLogLevel) -> bool>(
152        &self,
153        cond: C,
154        format: I,
155    ) {
156        if let Some(channel) = &self.log_channel
157            && cond(self.log_memory)
158        {
159            // Channel will never be full, don't care if it's closed.
160            let _ = channel.try_send(LogMessage::Memory(format()));
161        }
162    }
163
164    /// Log a failure at the moment it happens — the backstop of the lazy
165    /// model, for the failure nobody ever observes through a side effect.
166    ///
167    /// Always on, unlike the opt-in channels above: a kernel that will not
168    /// compile is a programming error, and a line at the failure site beats a
169    /// `Result` handed over some arbitrary distance later. Reads, syncs and
170    /// profiles still fail loudly on the buffers a failure claimed; this line
171    /// is for the launch whose output nobody ever asks about.
172    pub fn log_failure(&self, error: &impl Display) {
173        log::warn!("{error}");
174    }
175
176    /// Register a profiled task without timing.
177    pub fn register_execution(&self, name: impl Display) {
178        if let Some(channel) = &self.log_channel
179            && matches!(self.profile_level, Some(ProfileLevel::ExecutionOnly))
180        {
181            // Channel will never be full, don't care if it's closed.
182            let _ = channel.try_send(LogMessage::Execution(name.to_string()));
183        }
184    }
185
186    /// Register a profiled task.
187    pub fn register_profiled(&self, name: impl Display, duration: ProfileDuration) {
188        if let Some(channel) = &self.log_channel
189            && self.profile_level.is_some()
190        {
191            // Channel will never be full, don't care if it's closed.
192            let _ = channel.try_send(LogMessage::Profile(name.to_string(), duration));
193        }
194    }
195
196    /// Show the profiling summary if activated and reset its state.
197    pub fn profile_summary(&self) {
198        if let Some(channel) = &self.log_channel
199            && self.profile_level.is_some()
200        {
201            // Channel will never be full, don't care if it's closed.
202            let _ = channel.try_send(LogMessage::ProfileSummary);
203        }
204    }
205}
206
207struct AsyncLogger {
208    message: Receiver<LogMessage>,
209    logger: Logger,
210    profiled: Profiled,
211}
212
213impl AsyncLogger {
214    async fn process(mut self) {
215        while let Ok(msg) = self.message.recv().await {
216            match msg {
217                LogMessage::Compilation(msg) => {
218                    self.logger.log_compilation(&msg);
219                }
220                LogMessage::Streaming(msg) => {
221                    self.logger.log_streaming(&msg);
222                }
223                LogMessage::Memory(msg) => {
224                    self.logger.log_memory(&msg);
225                }
226                LogMessage::Profile(name, profile) => match profile.resolve().await {
227                    Some(ticks) => {
228                        let duration = ticks.duration();
229                        self.profiled.update(&name, duration);
230                        self.logger
231                            .log_profiling(&format!("| {duration:<10?} | {name}"));
232                    }
233                    // Named but not counted. Folding an absence into the
234                    // summary as a zero would drag every average it joins down
235                    // towards a speed nothing achieved.
236                    None => self
237                        .logger
238                        .log_profiling(&format!("| {:<10} | {name}", "not measured")),
239                },
240                LogMessage::Execution(name) => {
241                    self.logger.log_profiling(&format!("Executing {name}"));
242                }
243                LogMessage::ProfileSummary => {
244                    if !self.profiled.is_empty() {
245                        self.logger.log_profiling(&self.profiled);
246                        self.profiled = Profiled::default();
247                    }
248                }
249            }
250        }
251    }
252}