Skip to main content

cubecl_runtime/tune/
tuner.rs

1#[cfg(std_io)]
2use alloc::format;
3use alloc::sync::Arc;
4use alloc::vec::Vec;
5use cubecl_common::profile::ProfileDuration;
6use derive_more::Display;
7
8use core::time::Duration;
9
10use cubecl_environment::sync::Mutex;
11
12use alloc::string::{String, ToString};
13use cubecl_common::benchmark::{BenchmarkComputations, BenchmarkDurations};
14
15use crate::config::Logger;
16#[cfg(std_io)]
17use crate::config::autotune::AutotuneLogLevel;
18use crate::server::LaunchError;
19use crate::tune::{AutotuneLoggerExt, AutotuneResult, TimeBound, TuneCache, tune_benchmark};
20use crate::{client::ComputeClient, runtime::Runtime};
21use cubecl_environment::config::RuntimeConfig;
22
23use super::{
24    AutotuneKey, AutotuneOutput, TunableSet, TuneCacheResult, TuneFn, TuneInputs, TunePlan,
25};
26
27#[derive(Debug)]
28/// Runs autotune benchmarks for a single device and caches the results.
29///
30/// On wasm, [`tune`](Self::tune) spawns its work on the browser event loop; elsewhere
31/// it blocks inline. Either way the benchmarking itself is synchronous; only the
32/// per-sample profile resolution is awaited.
33pub struct Tuner<K: AutotuneKey> {
34    cache: Arc<Mutex<TuneCache<K>>>,
35    logger: Arc<Mutex<Logger>>,
36}
37
38/// The measured outcome for a given autotune invocation.
39#[cfg_attr(autotune_persistence, derive(serde::Serialize, serde::Deserialize))]
40#[derive(new, Debug, Clone, PartialEq, Eq)]
41pub struct AutotuneOutcome {
42    /// The name of the tunable.
43    pub name: String,
44    /// The index of the tunable.
45    pub index: usize,
46    /// The computation benchmark results.
47    pub computation: BenchmarkComputations,
48}
49
50impl core::fmt::Display for AutotuneOutcome {
51    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52        write!(
53            f,
54            "Autotune[{}] name {} => {:?}",
55            self.index, self.name, self.computation
56        )
57    }
58}
59
60/// Error from running autotune.
61#[derive(Clone, Display)]
62#[cfg_attr(autotune_persistence, derive(serde::Serialize, serde::Deserialize))]
63pub enum AutotuneError {
64    /// An unknown error happened.
65    #[display("{name}: An unknown error happened.\n{err}")]
66    Unknown {
67        /// The name of the tunable.
68        name: String,
69        /// The unknown error,
70        err: String,
71    },
72    /// All samples are invalid.
73    #[display("{name}: All samples are invalid.")]
74    InvalidSamples {
75        /// The name of the tunable.
76        name: String,
77    },
78    /// No autotune was flagged as valid for the problem.
79    ///
80    /// # Warning
81    ///
82    /// This is an unrecoverable error and will cause a panic.
83    #[display("No autotune was flagged as valid for the problem.\n{context}")]
84    NoValidKernelFound {
85        /// The formatted context on why no valid kernel was found.
86        context: String,
87    },
88    /// The autotune is skipped manually.
89    #[display("{name}: The autotune is skipped manually.")]
90    Skip {
91        /// The name of the skipped kernel.
92        name: String,
93    },
94
95    /// An error happened when launching a kernel.
96    Launch(LaunchError),
97}
98
99impl core::fmt::Debug for AutotuneError {
100    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
101        write!(f, "{self}")
102    }
103}
104
105impl From<LaunchError> for AutotuneError {
106    fn from(value: LaunchError) -> Self {
107        Self::Launch(value)
108    }
109}
110
111/// A successfully-queued benchmark: the profile futures for each sample, plus its metadata.
112struct PendingBench {
113    index: usize,
114    name: String,
115    profiles: Vec<ProfileDuration>,
116    /// Time spent launching, when steps are being logged. The samples are still unresolved at
117    /// that point, so the resolution wait is added in [`process_request`] before it is reported.
118    launch: Option<Duration>,
119}
120
121/// Everything a benchmarking strategy needs, prepared once by [`Tuner::check_tune`] and handed to
122/// whichever strategy runs. `'t` borrows the tunable set, `'i` the benchmark inputs.
123struct TuneJob<'t, 'i, K: AutotuneKey, F: TuneInputs, Out> {
124    key: K,
125    autotunables: Vec<&'t TuneFn<F, Out>>,
126    test_inputs: <F as TuneInputs>::At<'i>,
127    plan: TunePlan,
128    results: Vec<AutotuneResult>,
129    #[cfg(any(not(target_family = "wasm"), autotune_persistence))]
130    limit: Option<Duration>,
131    #[cfg(autotune_persistence)]
132    bounds: Option<crate::tune::Bounds>,
133    #[cfg(not(target_family = "wasm"))]
134    short_circuit: bool,
135    #[cfg(autotune_persistence)]
136    checksum: String,
137    log_context: Option<crate::tune::AutotuneLogContext>,
138}
139
140impl<K: AutotuneKey, F: TuneInputs, Out> TuneJob<'_, '_, K, F, Out> {
141    fn into_request(self, pending: Vec<PendingBench>, decided: Option<usize>) -> TuneRequest<K> {
142        TuneRequest {
143            key: self.key,
144            results: self.results,
145            #[cfg(autotune_persistence)]
146            checksum: self.checksum,
147            log_context: self.log_context,
148            pending,
149            decided,
150            #[cfg(autotune_persistence)]
151            limit: self.limit,
152            #[cfg(autotune_persistence)]
153            bounds: self.bounds,
154        }
155    }
156}
157
158/// A queued tuning job: all data needed to resolve samples and commit the result.
159/// Holds no references so it's trivially `Send + 'static` for the wasm spawn path.
160struct TuneRequest<K: AutotuneKey> {
161    key: K,
162    results: Vec<AutotuneResult>,
163    #[cfg(autotune_persistence)]
164    checksum: String,
165    log_context: Option<crate::tune::AutotuneLogContext>,
166    pending: Vec<PendingBench>,
167    /// The winner, when the strategy already picked one. `None` means the results are all
168    /// comparable and the fastest is whichever scores best.
169    decided: Option<usize>,
170    #[cfg(autotune_persistence)]
171    limit: Option<Duration>,
172    #[cfg(autotune_persistence)]
173    bounds: Option<crate::tune::Bounds>,
174}
175
176#[allow(clippy::new_without_default)]
177impl<K: AutotuneKey> Tuner<K> {
178    /// Create a tuner. Its cache is seeded from the persistent cache when
179    /// persistence is available (disk on native, browser storage on wasm with
180    /// the `browser-cache` feature).
181    pub fn new(name: &str, device_id: &str) -> Self {
182        Self {
183            cache: Arc::new(Mutex::new(TuneCache::new(name, device_id))),
184            logger: Arc::new(Mutex::new(Logger::new())),
185        }
186    }
187
188    /// Fetch the fastest autotune operation index for an autotune key.
189    ///
190    /// This resets the cache when the environment switched but does not
191    /// re-hydrate it from persistence, so right after a switch it reports a
192    /// [`Miss`](TuneCacheResult::Miss) even for keys the new environment has
193    /// cached. It is a fast-path probe: a miss here is expected to fall through
194    /// to [`check_tune`](Self::check_tune), which hydrates and resolves the
195    /// real state. Don't rely on it as a standalone "is this cached?" query.
196    pub fn fastest(&self, key: &K) -> TuneCacheResult {
197        #[cfg_attr(not(autotune_persistence), allow(unused_mut))]
198        let mut cache = self.cache.lock();
199        #[cfg(autotune_persistence)]
200        cache.reset_if_environment_switched();
201
202        cache.fastest(key)
203    }
204
205    /// Fetch the logger instance.
206    pub fn logger(&self) -> Arc<Mutex<Logger>> {
207        self.logger.clone()
208    }
209
210    /// Check the cache, validate checksums if needed, and kick off a tuning job if the
211    /// key is a miss. Returns the resolved cache state.
212    pub fn check_tune<'a, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
213        &self,
214        key: &K,
215        inputs: &F::At<'a>,
216        tunables: &TunableSet<K, F, Out>,
217        #[cfg_attr(not(autotune_persistence), allow(unused))] checksum: impl FnOnce() -> String
218        + Send
219        + Sync,
220        client: &ComputeClient<R>,
221        mut log_context: Option<crate::tune::AutotuneLogContext>,
222    ) -> TuneCacheResult
223    where
224        <F as TuneInputs>::At<'a>: Clone + Send,
225    {
226        {
227            let mut cache = self.cache.lock();
228            #[cfg(autotune_persistence)]
229            cache.reset_if_environment_switched();
230            let cur = cache.fastest(key);
231
232            // Browser hydration is asynchronous, so persistent entries may
233            // have arrived after construction. Ingest them before starting a
234            // redundant tune.
235            #[cfg(autotune_persistence)]
236            let cur = if matches!(cur, TuneCacheResult::Miss) {
237                cache.sync_persistent();
238                cache.fastest(key)
239            } else {
240                cur
241            };
242
243            #[cfg(autotune_persistence)]
244            let cur = if matches!(cur, TuneCacheResult::Unchecked) {
245                let mut log = self.logger.lock();
246                let checksum = checksum();
247                if let AutotuneLogLevel::Full = log.log_level_autotune() {
248                    log.log_autotune(&format!("validate checksum key={key}, checksum={checksum}"));
249                }
250                cache.validate_checksum(key, &checksum)
251            } else {
252                cur
253            };
254
255            match cur {
256                TuneCacheResult::Hit { .. } | TuneCacheResult::Pending => return cur,
257                TuneCacheResult::Miss | TuneCacheResult::Unchecked => {
258                    cache.mark_pending(key.clone())
259                }
260            }
261            // Scope the guard: the rest of this function re-locks `self.cache` (fast
262            // path insert, `process_request`), and the mutex is non-reentrant.
263        }
264
265        log::info!("Tuning {key}");
266
267        let autotunables = tunables.autotunables().collect::<Vec<_>>();
268        let results: Vec<AutotuneResult> = autotunables
269            .iter()
270            .map(|a| {
271                AutotuneResult::error(AutotuneError::Skip {
272                    name: a.name.to_string(),
273                })
274            })
275            .collect();
276
277        #[cfg(autotune_persistence)]
278        let checksum = tunables.compute_checksum();
279
280        // Fast path: single tunable, no benchmarking needed.
281        if results.len() == 1 {
282            self.cache.lock().cache_insert(key.clone(), 0);
283            return TuneCacheResult::Hit { fastest_index: 0 };
284        }
285
286        let test_inputs = tunables.generate_inputs(key, inputs);
287        let plan = tunables.plan(key);
288        let bounds = tunables.bounds(key, inputs);
289        let limit = bounds.as_ref().and_then(|bounds| bounds.time_limit());
290
291        log_context.set_bounds(bounds.clone());
292        log_context.set_limit(limit);
293
294        // The slowest median duration still considered close enough to peak throughput.
295        // Only used on native, where a benchmark can be resolved inline to exit early.
296        #[cfg(not(target_family = "wasm"))]
297        let short_circuit = limit.is_some()
298            && tunables.is_short_circuit_enabled()
299            && !crate::config::CubeClRuntimeConfig::get()
300                .autotune
301                .disable_short_circuit;
302
303        let job = TuneJob {
304            key: key.clone(),
305            autotunables,
306            test_inputs,
307            plan,
308            results,
309            #[cfg(any(not(target_family = "wasm"), autotune_persistence))]
310            limit,
311            #[cfg(autotune_persistence)]
312            bounds,
313            #[cfg(not(target_family = "wasm"))]
314            short_circuit,
315            #[cfg(autotune_persistence)]
316            checksum,
317            log_context,
318        };
319
320        #[cfg(not(target_family = "wasm"))]
321        if crate::config::CubeClRuntimeConfig::get()
322            .autotune
323            .bench
324            .adaptive
325        {
326            return self.tune_adaptive(job, client);
327        }
328
329        self.tune_fixed_samples(job, client)
330    }
331
332    /// Round robin the candidates, eliminating them as the evidence allows. Native only: the
333    /// driver has to resolve samples between rounds, which it cannot do on the browser event loop.
334    #[cfg(not(target_family = "wasm"))]
335    fn tune_adaptive<'i, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
336        &self,
337        mut job: TuneJob<'_, 'i, K, F, Out>,
338        client: &ComputeClient<R>,
339    ) -> TuneCacheResult
340    where
341        <F as TuneInputs>::At<'i>: Clone + Send,
342    {
343        let schedule = crate::tune::schedule::Schedule {
344            config: crate::config::CubeClRuntimeConfig::get()
345                .autotune
346                .bench
347                .clone(),
348            limit: job.limit,
349            short_circuit: job.short_circuit,
350            track_steps: job.log_context.is_some(),
351        };
352
353        let outcome = schedule.run_plan(
354            &job.key,
355            &mut job.plan,
356            &job.autotunables,
357            &job.test_inputs,
358            client,
359            &mut job.results,
360        );
361
362        for (name, duration) in outcome.steps {
363            job.log_context.push_tuning_step(name, duration);
364        }
365        if let Some(name) = outcome.short_circuit {
366            job.log_context.push_short_circuit(name);
367        }
368
369        let request = job.into_request(Vec::new(), outcome.decided);
370
371        cubecl_environment::future::block_on(process_request(request, &self.cache, &self.logger))
372    }
373
374    /// Benchmark every candidate with a fixed sample count, resolving the samples afterwards.
375    /// This is the only strategy available on wasm, where nothing can be awaited inline.
376    fn tune_fixed_samples<'i, R: Runtime, F: TuneInputs, Out: AutotuneOutput>(
377        &self,
378        mut job: TuneJob<'_, 'i, K, F, Out>,
379        client: &ComputeClient<R>,
380    ) -> TuneCacheResult
381    where
382        <F as TuneInputs>::At<'i>: Clone + Send,
383    {
384        // The batch-retry check below reads this through `cfg!`, which keeps
385        // the name alive on wasm too; the assignment is native-only, so it
386        // simply stays false there.
387        #[cfg(not(target_family = "wasm"))]
388        let mut batch_success = false;
389        #[cfg(target_family = "wasm")]
390        let batch_success = false;
391
392        // Walk the plan batch by batch, launching each benchmark synchronously. A
393        // successful launch queues a `PendingBench` for the async resolver below;
394        // launch errors go straight into `results`. Retry the next batch if a whole
395        // batch failed to queue anything.
396        let mut pending = Vec::<PendingBench>::new();
397        loop {
398            let tunable_indices = job.plan.next();
399
400            if tunable_indices.is_empty() {
401                let key = &job.key;
402                panic!(
403                    "Can't execute the autotune plan for key: {key:?}\n - plan: {:?}\n - results: {:?}",
404                    job.plan, job.results
405                );
406            }
407
408            for index in tunable_indices {
409                let op = job.autotunables[index];
410
411                let start_time = job
412                    .log_context
413                    .is_some()
414                    .then(cubecl_common::profile::Instant::now);
415
416                match tune_benchmark(op, job.test_inputs.clone(), client.clone()) {
417                    Ok(profiles) => {
418                        let bench = PendingBench {
419                            index,
420                            name: op.name.clone(),
421                            profiles,
422                            launch: start_time.map(|start| start.elapsed()),
423                        };
424
425                        #[cfg(not(target_family = "wasm"))]
426                        if job.short_circuit {
427                            let result = cubecl_environment::future::block_on(resolve_bench(bench));
428
429                            // short_circuit is only true when limit.is_some() => unwrap is fine.
430                            let close_enough = result
431                                .outcome
432                                .as_ref()
433                                .is_ok_and(|out| out.computation.median <= job.limit.unwrap());
434
435                            batch_success |= result.outcome.is_ok();
436                            job.results[index] = result;
437
438                            if let Some(start) = start_time {
439                                job.log_context
440                                    .push_tuning_step(op.name.to_string(), start.elapsed());
441                            }
442
443                            if close_enough {
444                                job.log_context.push_short_circuit(op.name.to_string());
445                                break;
446                            }
447
448                            continue;
449                        }
450
451                        // The step is reported once `process_request` has resolved the samples,
452                        // so the logged duration covers benchmarking and not just the launch.
453                        pending.push(bench);
454                    }
455                    Err(err) => {
456                        job.results[index] = AutotuneResult::error(err);
457                        if let Some(start) = start_time {
458                            job.log_context
459                                .push_tuning_step(op.name.to_string(), start.elapsed());
460                        }
461                    }
462                }
463            }
464
465            #[cfg(not(target_family = "wasm"))]
466            if !pending.is_empty() || batch_success {
467                break;
468            }
469            #[cfg(target_family = "wasm")]
470            if !pending.is_empty() {
471                break;
472            }
473        }
474
475        // Every candidate here carries the same sample count, so scoring them against each other
476        // is a fair comparison and `process_request` can make the call.
477        let request = job.into_request(pending, None);
478
479        // Resolve samples and commit the result. On wasm this runs on the browser
480        // event loop; elsewhere it blocks inline.
481        #[cfg(target_family = "wasm")]
482        {
483            let cache = self.cache.clone();
484            let logger = self.logger.clone();
485            wasm_bindgen_futures::spawn_local(async move {
486                process_request(request, &cache, &logger).await;
487            });
488
489            return TuneCacheResult::Pending;
490        }
491
492        #[cfg(not(target_family = "wasm"))]
493        cubecl_environment::future::block_on(process_request(request, &self.cache, &self.logger))
494    }
495}
496
497/// Await every sample of a single benchmark and fold them into one result.
498///
499/// The samples are resolved concurrently: a profile only submits its readback when
500/// first polled, so awaiting them one by one would serialize a device round-trip per
501/// sample.
502async fn resolve_bench(bench: PendingBench) -> AutotuneResult {
503    let PendingBench {
504        index,
505        name,
506        profiles,
507        launch: _,
508    } = bench;
509
510    let Some(first) = profiles.first() else {
511        return AutotuneResult::error(AutotuneError::Unknown {
512            name: name.to_string(),
513            err: "No profiling available".to_string(),
514        });
515    };
516    let timing_method = first.timing_method();
517
518    let durations: Vec<Duration> =
519        futures_util::future::join_all(profiles.into_iter().map(ProfileDuration::resolve))
520            .await
521            .into_iter()
522            .map(|ticks| ticks.duration())
523            .collect();
524
525    AutotuneResult::success(AutotuneOutcome::new(
526        name,
527        index,
528        BenchmarkComputations::new(&BenchmarkDurations::from_durations(
529            timing_method,
530            durations,
531        )),
532    ))
533}
534
535/// Await every profile sample, pick the fastest tunable, commit to the cache.
536async fn process_request<K: AutotuneKey>(
537    request: TuneRequest<K>,
538    cache: &Mutex<TuneCache<K>>,
539    logger: &Mutex<Logger>,
540) -> TuneCacheResult {
541    let TuneRequest {
542        key,
543        mut results,
544        #[cfg(autotune_persistence)]
545        checksum,
546        mut log_context,
547        pending,
548        decided,
549        #[cfg(autotune_persistence)]
550        limit,
551        #[cfg(autotune_persistence)]
552        bounds,
553    } = request;
554
555    // Resolved concurrently, and each benchmark timed individually rather than timing the loop:
556    // the profiles were all queued before any of them was polled, so awaiting them in turn would
557    // charge the first benchmark for draining the whole device queue and report the rest as
558    // free.
559    let resolved = futures_util::future::join_all(pending.into_iter().map(|bench| {
560        let index = bench.index;
561        let name = bench.name.clone();
562        let launch = bench.launch;
563
564        async move {
565            let started = cubecl_common::profile::Instant::now();
566            let result = resolve_bench(bench).await;
567            let step = launch.map(|launch| (name, launch + started.elapsed()));
568
569            (index, step, result)
570        }
571    }))
572    .await;
573
574    for (index, step, result) in resolved {
575        if let Some((name, duration)) = step {
576            log_context.push_tuning_step(name, duration);
577        }
578
579        results[index] = result;
580    }
581
582    results.sort_by(|a, b| {
583        let a = a
584            .outcome
585            .as_ref()
586            .map(|r| r.computation.score())
587            .unwrap_or(u64::MAX);
588        let b = b
589            .outcome
590            .as_ref()
591            .map(|r| r.computation.score())
592            .unwrap_or(u64::MAX);
593        a.cmp(&b)
594    });
595
596    // The sort above orders what gets logged and persisted. It does not pick the winner when the
597    // strategy already did: a scheduler that eliminates candidates leaves results built from
598    // different sample counts behind, and `score` reads a short sample set as a stable one.
599    let fastest_index = match decided {
600        Some(index) => index,
601        None => {
602            results
603                .first()
604                .expect("At least one kernel needed.")
605                .outcome
606                .as_ref()
607                .expect("At least one kernel has to succeed.")
608                .index
609        }
610    };
611
612    {
613        log_context.log_result(&mut logger.lock(), &key, &results);
614        cache.lock().cache_insert(key.clone(), fastest_index);
615        #[cfg(autotune_persistence)]
616        cache.lock().persistent_cache_insert(
617            key,
618            checksum,
619            crate::tune::PersistentCacheValue {
620                fastest_index,
621                results,
622                bounds,
623                limit,
624            },
625        );
626    }
627
628    TuneCacheResult::Hit { fastest_index }
629}
630
631#[cfg(feature = "autotune-checks")]
632pub(crate) fn check_autotune_outputs<O: AutotuneOutput>(
633    mut checks_outputs: Vec<(String, Result<O, AutotuneError>)>,
634) -> Vec<crate::tune::log::CheckResult> {
635    if checks_outputs.is_empty() {
636        return Vec::new();
637    }
638
639    let reference_idx = checks_outputs
640        .iter()
641        .position(|(_, res)| res.is_ok())
642        .unwrap_or(checks_outputs.len() - 1);
643    let reference = checks_outputs.remove(reference_idx);
644    let reference_result = reference.1;
645    #[cfg(std_io)]
646    let reference_name = reference.0;
647
648    let is_recording = is_recording_enabled();
649
650    #[cfg(std_io)]
651    {
652        let reference_passed = reference_result.is_ok();
653        let mut check_results = execute_checks(checks_outputs, reference_result, is_recording);
654        check_results.push(crate::tune::log::CheckResult {
655            name: reference_name,
656            passed: reference_passed,
657        });
658
659        check_results
660    }
661
662    #[cfg(not(std_io))]
663    {
664        execute_checks(checks_outputs, reference_result, is_recording)
665    }
666}
667
668/// Whether a mismatch should be collected rather than fatal: it can only be reported if something
669/// is recording the results, so with no recorder a failed check panics on the spot instead of
670/// passing silently.
671#[cfg(feature = "autotune-checks")]
672fn is_recording_enabled() -> bool {
673    crate::config::CubeClRuntimeConfig::get()
674        .autotune
675        .recording_enabled()
676}
677
678#[cfg(feature = "autotune-checks")]
679fn execute_checks<O: AutotuneOutput>(
680    checks_outputs: Vec<(String, Result<O, AutotuneError>)>,
681    reference_result: Result<O, AutotuneError>,
682    is_recording: bool,
683) -> Vec<crate::tune::log::CheckResult> {
684    let mut check_results = Vec::new();
685
686    let Ok(reference) = reference_result else {
687        for (name, _) in checks_outputs.into_iter() {
688            check_results.push(crate::tune::log::CheckResult {
689                name,
690                passed: false,
691            });
692        }
693        return check_results;
694    };
695
696    for (name, other_result) in checks_outputs.into_iter() {
697        if let Ok(other) = other_result {
698            let passed = check_equivalence(&reference, other, is_recording);
699            check_results.push(crate::tune::log::CheckResult { name, passed });
700        } else {
701            check_results.push(crate::tune::log::CheckResult {
702                name,
703                passed: false,
704            });
705        }
706    }
707
708    check_results
709}
710
711#[cfg(feature = "autotune-checks")]
712fn check_equivalence<O: AutotuneOutput>(reference: &O, other: O, is_recording: bool) -> bool {
713    // When the results are being recorded, we catch the panic so we can collect and report every
714    // check failure. With nothing recording, we let it panic immediately rather than pass silently.
715    if is_recording {
716        #[cfg(std_io)]
717        {
718            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
719                reference.check_equivalence(other);
720            }))
721            .is_ok()
722        }
723        #[cfg(not(std_io))]
724        {
725            reference.check_equivalence(other);
726            true
727        }
728    } else {
729        reference.check_equivalence(other);
730        true
731    }
732}