1use alloc::boxed::Box;
2#[cfg(persistence)]
3use alloc::format;
4use alloc::sync::Arc;
5use alloc::vec::Vec;
6use cubecl_common::profile::ProfileDuration;
7use derive_more::Display;
8
9use core::time::Duration;
10
11use cubecl_environment::sync::Mutex;
12
13use alloc::string::{String, ToString};
14use cubecl_common::benchmark::{BenchmarkComputations, BenchmarkDurations};
15
16use crate::client::Client;
17use crate::config::Logger;
18#[cfg(persistence)]
19use crate::config::autotune::AutotuneLogLevel;
20use crate::device::HardwareIdentity;
21use crate::execution::{ProcessMode, TuneOutcome, TuneRegistration};
22use crate::server::LaunchError;
23#[cfg(not(target_family = "wasm"))]
24use crate::server::ServerError;
25use crate::tune::sampler::measured;
26use crate::tune::{AutotuneLoggerExt, AutotuneResult, TimeBound, TuneCache, tune_benchmark};
27use cubecl_environment::config::RuntimeConfig;
28
29use super::{
30 AutotuneKey, AutotuneOutput, TunableSet, TuneCacheResult, TuneFn, TuneInputs, TunePlan,
31};
32
33#[derive(Debug)]
34pub struct Tuner<K: AutotuneKey> {
40 cache: Arc<Mutex<TuneCache<K>>>,
41 logger: Arc<Mutex<Logger>>,
42}
43
44#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
46#[derive(new, Debug, Clone, PartialEq, Eq)]
47pub struct AutotuneOutcome {
48 pub name: String,
50 pub index: usize,
52 pub computation: BenchmarkComputations,
54}
55
56impl core::fmt::Display for AutotuneOutcome {
57 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
58 write!(
59 f,
60 "Autotune[{}] name {} => {:?}",
61 self.index, self.name, self.computation
62 )
63 }
64}
65
66#[derive(Clone, Display)]
68#[cfg_attr(serializable, derive(serde::Serialize, serde::Deserialize))]
69pub enum AutotuneError {
70 #[display("{name}: An unknown error happened.\n{err}")]
72 Unknown {
73 name: String,
75 err: String,
77 },
78 #[display("{name}: Rejected the problem.\n{reason}")]
84 Rejected {
85 name: String,
87 reason: String,
89 },
90 #[display("{name}: All samples are invalid.")]
92 InvalidSamples {
93 name: String,
95 },
96 #[display("No autotune was flagged as valid for the problem.\n{context}")]
102 NoValidKernelFound {
103 context: String,
105 },
106 #[display("{name}: A profiled sample carried no measurement.")]
112 NotMeasured {
113 name: String,
115 },
116 #[display("{name}: The autotune is skipped manually.")]
118 Skip {
119 name: String,
121 },
122
123 Launch(LaunchError),
125}
126
127impl core::fmt::Debug for AutotuneError {
128 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
129 write!(f, "{self}")
130 }
131}
132
133impl From<LaunchError> for AutotuneError {
134 fn from(value: LaunchError) -> Self {
135 Self::Launch(value)
136 }
137}
138
139struct PendingBench {
141 index: usize,
142 name: String,
143 profiles: Vec<ProfileDuration>,
144 launch: Option<Duration>,
147}
148
149struct TuneJob<'t, 'i, K: AutotuneKey, F: TuneInputs, Out> {
152 key: K,
153 autotunables: Vec<&'t TuneFn<F, Out>>,
154 test_inputs: <F as TuneInputs>::At<'i>,
155 evictor: Option<Box<crate::tune::Evictor<'i>>>,
157 plan: TunePlan,
158 results: Vec<AutotuneResult>,
159 #[cfg(any(not(target_family = "wasm"), persistence))]
160 limit: Option<Duration>,
161 #[cfg(persistence)]
162 bounds: Option<crate::tune::Bounds>,
163 #[cfg(not(target_family = "wasm"))]
164 short_circuit: bool,
165 #[cfg(persistence)]
166 checksum: String,
167 log_context: Option<crate::tune::AutotuneLogContext>,
168 #[cfg(persistence)]
169 recording: crate::tune::record::TuneRecording<K>,
170 registration: Option<TuneRegistration>,
172 mark: PendingMark<K>,
173}
174
175impl<K: AutotuneKey, F: TuneInputs, Out> TuneJob<'_, '_, K, F, Out> {
176 fn into_request(self, pending: Vec<PendingBench>, decided: Option<usize>) -> TuneRequest<K> {
177 TuneRequest {
178 key: self.key,
179 results: self.results,
180 #[cfg(persistence)]
181 checksum: self.checksum,
182 log_context: self.log_context,
183 pending,
184 decided,
185 #[cfg(persistence)]
186 limit: self.limit,
187 #[cfg(persistence)]
188 bounds: self.bounds,
189 #[cfg(persistence)]
190 recording: self.recording,
191 registration: self.registration,
192 mark: self.mark,
193 }
194 }
195}
196
197struct TuneRequest<K: AutotuneKey> {
200 key: K,
201 results: Vec<AutotuneResult>,
202 #[cfg(persistence)]
203 checksum: String,
204 log_context: Option<crate::tune::AutotuneLogContext>,
205 pending: Vec<PendingBench>,
206 decided: Option<usize>,
209 #[cfg(persistence)]
210 limit: Option<Duration>,
211 #[cfg(persistence)]
212 bounds: Option<crate::tune::Bounds>,
213 #[cfg(persistence)]
214 recording: crate::tune::record::TuneRecording<K>,
215 registration: Option<TuneRegistration>,
216 mark: PendingMark<K>,
217}
218
219struct PendingMark<K: AutotuneKey> {
223 cache: Arc<Mutex<TuneCache<K>>>,
224 key: K,
225 #[cfg(persistence)]
227 generation: u32,
228}
229
230impl<K: AutotuneKey> Drop for PendingMark<K> {
231 fn drop(&mut self) {
232 let mut cache = self.cache.lock();
233 #[cfg(persistence)]
234 if cache.generation() != self.generation {
235 return;
236 }
237 cache.unmark_pending(&self.key);
238 }
239}
240
241#[allow(clippy::new_without_default)]
242impl<K: AutotuneKey> Tuner<K> {
243 pub fn new(name: &str, hardware: &HardwareIdentity) -> Self {
248 Self {
249 cache: Arc::new(Mutex::new(TuneCache::new(name, hardware))),
250 logger: Arc::new(Mutex::new(Logger::new())),
251 }
252 }
253
254 pub fn fastest(&self, key: &K) -> TuneCacheResult {
262 #[cfg_attr(not(persistence), allow(unused_mut))]
263 let mut cache = self.cache.lock();
264 #[cfg(persistence)]
265 cache.reset_if_environment_switched();
266
267 cache.fastest(key)
268 }
269
270 pub(crate) fn settled(&self, key: &K) -> Option<usize> {
274 self.cache.lock().settled(key)
275 }
276
277 pub fn logger(&self) -> Arc<Mutex<Logger>> {
279 self.logger.clone()
280 }
281
282 pub fn check_tune<'a, F: TuneInputs, Out: AutotuneOutput, Id>(
285 &self,
286 key: &K,
287 inputs: &F::At<'a>,
288 tunables: &TunableSet<K, F, Out, Id>,
289 client: &Client,
290 mut log_context: Option<crate::tune::AutotuneLogContext>,
291 ) -> TuneCacheResult
292 where
293 <F as TuneInputs>::At<'a>: Clone + Send,
294 {
295 let compiling = ProcessMode::current() == ProcessMode::CompileOnly;
296 #[cfg(persistence)]
297 let checksum;
298
299 let mut gathered = None;
301 let mark = {
302 let mut cache = self.cache.lock();
305 #[cfg(persistence)]
306 cache.reset_if_environment_switched();
307 if compiling && cache.is_compiled(key, client.service_id()) {
312 return TuneCacheResult::Compiled;
313 }
314 let cur = cache.fastest(key);
315 if matches!(cur, TuneCacheResult::Hit { .. } | TuneCacheResult::Pending) {
316 return cur;
317 }
318 if cache.is_lost(client.service_id()) {
320 return TuneCacheResult::Pending;
321 }
322 if tunables.autotunables().nth(1).is_none() {
324 cache.cache_insert(key.clone(), 0);
325 return TuneCacheResult::Hit { fastest_index: 0 };
326 }
327
328 #[cfg(persistence)]
329 {
330 checksum = tunables.compute_checksum();
331 let stored = cache.look_up(key, &checksum);
332 let mut log = self.logger.lock();
333 if let AutotuneLogLevel::Full = log.log_level_autotune() {
334 log.log_autotune(&format!(
335 "look up key={key}, checksum={checksum}: {stored:?}"
336 ));
337 }
338 if let TuneCacheResult::Hit { .. } = stored {
339 return stored;
340 }
341 }
342
343 if compiling {
352 cache.mark_compiled(key.clone(), client.service_id(), || {
353 if super::gathering::CandidateGathering::active() {
354 None
355 } else {
356 TuneRegistration::register()
357 }
358 });
359 None
360 } else {
361 gathered = cache.mark_pending(key.clone());
362 Some(PendingMark {
365 cache: self.cache.clone(),
366 key: key.clone(),
367 #[cfg(persistence)]
368 generation: cache.generation(),
369 })
370 }
371 };
372
373 log::info!("Tuning {key}");
374
375 let autotunables = tunables.autotunables().collect::<Vec<_>>();
376
377 let Some(mark) = mark else {
380 let gathering = super::gathering::CandidateGathering::new();
381 self.compile_plan(key, inputs, tunables, &autotunables);
382 core::mem::drop(gathering);
383 return TuneCacheResult::Compiled;
384 };
385 let registration = gathered.or_else(TuneRegistration::register);
387
388 let results: Vec<AutotuneResult> = autotunables
389 .iter()
390 .map(|a| {
391 AutotuneResult::error(AutotuneError::Skip {
392 name: a.name.to_string(),
393 })
394 })
395 .collect();
396
397 #[cfg(persistence)]
398 let mut recording =
399 crate::tune::record::TuneRecording::new(&self.cache.lock(), key, &checksum);
400 #[cfg(persistence)]
403 if recording.is_open() {
404 log_context.get_or_insert_with(Default::default);
405 }
406
407 let test_inputs = tunables.generate_inputs(key, inputs);
408 client.compile_queued();
412 let plan = tunables.plan(key);
413 #[cfg(persistence)]
414 if recording.is_open() {
415 let names: Vec<&str> = autotunables.iter().map(|tune| tune.name.as_str()).collect();
416 recording.plan(&plan, &names);
417 }
418 let bounds = tunables.bounds(key, inputs);
419 let limit = bounds.as_ref().and_then(|bounds| bounds.time_limit());
420
421 log_context.set_bounds(bounds.clone());
422 log_context.set_limit(limit);
423
424 #[cfg(not(target_family = "wasm"))]
427 let short_circuit = limit.is_some()
428 && tunables.is_short_circuit_enabled()
429 && !crate::config::CubeClRuntimeConfig::get()
430 .autotune
431 .disable_short_circuit;
432
433 let job = TuneJob {
434 key: key.clone(),
435 autotunables,
436 test_inputs,
437 evictor: tunables.evictor(key, inputs),
438 plan,
439 results,
440 #[cfg(any(not(target_family = "wasm"), persistence))]
441 limit,
442 #[cfg(persistence)]
443 bounds,
444 #[cfg(not(target_family = "wasm"))]
445 short_circuit,
446 #[cfg(persistence)]
447 checksum,
448 log_context,
449 #[cfg(persistence)]
450 recording,
451 registration,
452 mark,
453 };
454
455 #[cfg(not(target_family = "wasm"))]
456 if crate::config::CubeClRuntimeConfig::get()
457 .autotune
458 .bench
459 .adaptive
460 {
461 return self.tune_adaptive(job, client);
462 }
463
464 self.tune_fixed_samples(job, client)
465 }
466
467 fn compile_plan<'a, F: TuneInputs, Out: AutotuneOutput, Id>(
475 &self,
476 key: &K,
477 inputs: &F::At<'a>,
478 tunables: &TunableSet<K, F, Out, Id>,
479 autotunables: &[&TuneFn<F, Out>],
480 ) where
481 <F as TuneInputs>::At<'a>: Clone + Send,
482 {
483 let test_inputs = tunables.generate_inputs(key, inputs);
484 let mut plan = tunables.plan(key);
485 loop {
486 let batch = plan.next();
487 if batch.is_empty() {
488 break;
489 }
490 let mut served = false;
492 for index in batch.indices() {
493 served |= autotunables[index].execute(test_inputs.clone()).is_ok();
494 }
495 if served {
496 break;
497 }
498 }
499 }
500
501 #[cfg(not(target_family = "wasm"))]
504 fn tune_adaptive<'i, F: TuneInputs, Out: AutotuneOutput>(
505 &self,
506 mut job: TuneJob<'_, 'i, K, F, Out>,
507 client: &Client,
508 ) -> TuneCacheResult
509 where
510 <F as TuneInputs>::At<'i>: Clone + Send,
511 {
512 let config = crate::config::CubeClRuntimeConfig::get();
513 let mut schedule = crate::tune::schedule::Schedule {
514 config: config.autotune.bench.clone(),
515 limit: job.limit,
516 short_circuit: job.short_circuit,
517 track_steps: job.log_context.is_some(),
518 compile_parallelism: config.compilation.parallelism(),
519 evictor: job.evictor.take(),
520 };
521
522 let outcome = match schedule.run_plan(
523 &job.key,
524 &mut job.plan,
525 &job.autotunables,
526 &job.test_inputs,
527 client,
528 &mut job.results,
529 ) {
530 Ok(outcome) => outcome,
531 Err(lost) => return self.stop_on_lost_card(&job.key, client, lost),
532 };
533
534 for (name, duration) in outcome.steps {
535 job.log_context.push_tuning_step(name, duration);
536 }
537 if let Some(name) = outcome.short_circuit {
538 job.log_context.push_short_circuit(name);
539 }
540
541 let request = job.into_request(Vec::new(), outcome.decided);
542
543 cubecl_environment::future::block_on(process_request(request, &self.cache, &self.logger))
544 }
545
546 #[cfg(not(target_family = "wasm"))]
551 fn stop_on_lost_card(&self, key: &K, client: &Client, lost: ServerError) -> TuneCacheResult {
552 log::error!("Autotune of {key} stopped, the device is lost: {lost}");
553 self.cache.lock().mark_lost(client.service_id());
554 TuneCacheResult::Pending
555 }
556
557 fn tune_fixed_samples<'i, F: TuneInputs, Out: AutotuneOutput>(
560 &self,
561 mut job: TuneJob<'_, 'i, K, F, Out>,
562 client: &Client,
563 ) -> TuneCacheResult
564 where
565 <F as TuneInputs>::At<'i>: Clone + Send,
566 {
567 #[cfg(not(target_family = "wasm"))]
571 let mut batch_success = false;
572 #[cfg(target_family = "wasm")]
573 let batch_success = false;
574
575 let mut pending = Vec::<PendingBench>::new();
580 loop {
581 let batch = job.plan.next();
582
583 if batch.is_empty() {
584 #[cfg(not(target_family = "wasm"))]
585 if let Err(lost) = cubecl_environment::future::block_on(client.sync())
586 && lost.is_device_poisoned()
587 {
588 return self.stop_on_lost_card(&job.key, client, lost);
589 }
590 let key = &job.key;
591 panic!(
592 "Can't execute the autotune plan for key: {key:?}\n - plan: {:?}\n - results: {:?}",
593 job.plan, job.results
594 );
595 }
596
597 for index in batch.indices() {
599 let op = job.autotunables[index];
600
601 let start_time = job
602 .log_context
603 .is_some()
604 .then(cubecl_common::profile::Instant::now);
605
606 match tune_benchmark(
607 op,
608 job.test_inputs.clone(),
609 client.clone(),
610 job.evictor.as_deref_mut(),
611 ) {
612 Ok(profiles) => {
613 let bench = PendingBench {
614 index,
615 name: op.name.clone(),
616 profiles,
617 launch: start_time.map(|start| start.elapsed()),
618 };
619
620 #[cfg(not(target_family = "wasm"))]
621 if job.short_circuit {
622 let result = cubecl_environment::future::block_on(resolve_bench(bench));
623
624 let close_enough = result
626 .outcome
627 .as_ref()
628 .is_ok_and(|out| out.computation.median <= job.limit.unwrap());
629
630 batch_success |= result.outcome.is_ok();
631 job.results[index] = result;
632
633 if let Some(start) = start_time {
634 job.log_context
635 .push_tuning_step(op.name.to_string(), start.elapsed());
636 }
637
638 if close_enough {
639 job.log_context.push_short_circuit(op.name.to_string());
640 break;
641 }
642
643 continue;
644 }
645
646 pending.push(bench);
649 }
650 Err(err) => {
651 job.results[index] = AutotuneResult::error(err);
652 if let Some(start) = start_time {
653 job.log_context
654 .push_tuning_step(op.name.to_string(), start.elapsed());
655 }
656 }
657 }
658 }
659
660 #[cfg(not(target_family = "wasm"))]
661 if !pending.is_empty() || batch_success {
662 break;
663 }
664 #[cfg(target_family = "wasm")]
665 if !pending.is_empty() {
666 break;
667 }
668 }
669
670 let request = job.into_request(pending, None);
673
674 #[cfg(target_family = "wasm")]
677 {
678 let cache = self.cache.clone();
679 let logger = self.logger.clone();
680 wasm_bindgen_futures::spawn_local(async move {
681 process_request(request, &cache, &logger).await;
682 });
683
684 return TuneCacheResult::Pending;
685 }
686
687 #[cfg(not(target_family = "wasm"))]
688 cubecl_environment::future::block_on(process_request(request, &self.cache, &self.logger))
689 }
690}
691
692async fn resolve_bench(bench: PendingBench) -> AutotuneResult {
698 let PendingBench {
699 index,
700 name,
701 profiles,
702 launch: _,
703 } = bench;
704
705 let Some(first) = profiles.first() else {
706 return AutotuneResult::error(AutotuneError::Unknown {
707 name: name.to_string(),
708 err: "No profiling available".to_string(),
709 });
710 };
711 let timing_method = first.timing_method();
712
713 let Some(durations) =
717 futures_util::future::join_all(profiles.into_iter().map(ProfileDuration::resolve))
718 .await
719 .into_iter()
720 .map(measured)
721 .collect::<Option<Vec<Duration>>>()
722 else {
723 return AutotuneResult::error(AutotuneError::NotMeasured {
724 name: name.to_string(),
725 });
726 };
727
728 AutotuneResult::success(AutotuneOutcome::new(
729 name,
730 index,
731 BenchmarkComputations::new(&BenchmarkDurations::from_durations(
732 timing_method,
733 durations,
734 )),
735 ))
736}
737
738async fn process_request<K: AutotuneKey>(
740 request: TuneRequest<K>,
741 cache: &Mutex<TuneCache<K>>,
742 logger: &Mutex<Logger>,
743) -> TuneCacheResult {
744 let TuneRequest {
745 key,
746 mut results,
747 #[cfg(persistence)]
748 checksum,
749 mut log_context,
750 pending,
751 decided,
752 #[cfg(persistence)]
753 limit,
754 #[cfg(persistence)]
755 bounds,
756 #[cfg(persistence)]
757 recording,
758 registration,
759 mark,
760 } = request;
761
762 let resolved = futures_util::future::join_all(pending.into_iter().map(|bench| {
767 let index = bench.index;
768 let name = bench.name.clone();
769 let launch = bench.launch;
770
771 async move {
772 let started = cubecl_common::profile::Instant::now();
773 let result = resolve_bench(bench).await;
774 let step = launch.map(|launch| (name, launch + started.elapsed()));
775
776 (index, step, result)
777 }
778 }))
779 .await;
780
781 for (index, step, result) in resolved {
782 if let Some((name, duration)) = step {
783 log_context.push_tuning_step(name, duration);
784 }
785
786 results[index] = result;
787 }
788
789 let unmeasured = decided.is_some_and(|index| results[index].outcome.is_err());
793
794 results.sort_by(|a, b| {
795 let a = a
796 .outcome
797 .as_ref()
798 .map(|r| r.computation.score())
799 .unwrap_or(u64::MAX);
800 let b = b
801 .outcome
802 .as_ref()
803 .map(|r| r.computation.score())
804 .unwrap_or(u64::MAX);
805 a.cmp(&b)
806 });
807
808 let fastest_index = match decided {
812 Some(index) => index,
813 None => {
814 results
815 .first()
816 .expect("At least one kernel needed.")
817 .outcome
818 .as_ref()
819 .expect("At least one kernel has to succeed.")
820 .index
821 }
822 };
823
824 {
825 log_context.log_result(&mut logger.lock(), &key, &results);
826 cache.lock().cache_insert(key.clone(), fastest_index);
829 let outcome = if unmeasured {
831 TuneOutcome::Failed
832 } else {
833 TuneOutcome::Measured
834 };
835 #[cfg_attr(not(persistence), allow(unused_variables, unused_mut))]
836 let mut settled = registration.map(|registration| registration.settle(outcome));
837
838 #[cfg(persistence)]
845 let stored = !unmeasured
846 && cache.lock().persistent_cache_insert(
847 key,
848 checksum,
849 crate::tune::PersistentCacheValue {
850 fastest_index,
851 results,
852 bounds,
853 limit,
854 },
855 );
856
857 #[cfg(persistence)]
858 if stored && let Some(settled) = settled.as_mut() {
859 settled.record(crate::execution::TunePick::Persisted);
860 }
861
862 #[cfg(persistence)]
863 recording.finish(
864 crate::tune::record::Answer {
865 winner: fastest_index,
866 stored,
867 },
868 log_context.as_ref(),
869 );
870 }
871 core::mem::drop(mark);
872
873 TuneCacheResult::Hit { fastest_index }
874}
875
876#[cfg(feature = "autotune-checks")]
877pub(crate) fn check_autotune_outputs<O: AutotuneOutput>(
878 mut checks_outputs: Vec<(String, Result<O, AutotuneError>)>,
879) -> Vec<crate::tune::log::CheckResult> {
880 if checks_outputs.is_empty() {
881 return Vec::new();
882 }
883
884 let reference_idx = checks_outputs
885 .iter()
886 .position(|(_, res)| res.is_ok())
887 .unwrap_or(checks_outputs.len() - 1);
888 let reference = checks_outputs.remove(reference_idx);
889 let reference_result = reference.1;
890 #[cfg(std_io)]
891 let reference_name = reference.0;
892
893 let decisions_enabled = is_decisions_enabled();
894
895 #[cfg(std_io)]
896 {
897 let reference_passed = reference_result.is_ok();
898 let mut check_results = execute_checks(checks_outputs, reference_result, decisions_enabled);
899 check_results.push(crate::tune::log::CheckResult {
900 name: reference_name,
901 passed: reference_passed,
902 });
903
904 check_results
905 }
906
907 #[cfg(not(std_io))]
908 {
909 execute_checks(checks_outputs, reference_result, decisions_enabled)
910 }
911}
912
913#[cfg(feature = "autotune-checks")]
917fn is_decisions_enabled() -> bool {
918 crate::config::CubeClRuntimeConfig::get()
919 .autotune
920 .decisions_enabled()
921}
922
923#[cfg(feature = "autotune-checks")]
924fn execute_checks<O: AutotuneOutput>(
925 checks_outputs: Vec<(String, Result<O, AutotuneError>)>,
926 reference_result: Result<O, AutotuneError>,
927 decisions_enabled: bool,
928) -> Vec<crate::tune::log::CheckResult> {
929 let mut check_results = Vec::new();
930
931 let Ok(reference) = reference_result else {
932 for (name, _) in checks_outputs.into_iter() {
933 check_results.push(crate::tune::log::CheckResult {
934 name,
935 passed: false,
936 });
937 }
938 return check_results;
939 };
940
941 for (name, other_result) in checks_outputs.into_iter() {
942 if let Ok(other) = other_result {
943 let passed = check_equivalence(&reference, other, decisions_enabled);
944 check_results.push(crate::tune::log::CheckResult { name, passed });
945 } else {
946 check_results.push(crate::tune::log::CheckResult {
947 name,
948 passed: false,
949 });
950 }
951 }
952
953 check_results
954}
955
956#[cfg(feature = "autotune-checks")]
957fn check_equivalence<O: AutotuneOutput>(reference: &O, other: O, decisions_enabled: bool) -> bool {
958 if decisions_enabled {
961 #[cfg(std_io)]
962 {
963 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
964 reference.check_equivalence(other);
965 }))
966 .is_ok()
967 }
968 #[cfg(not(std_io))]
969 {
970 reference.check_equivalence(other);
971 true
972 }
973 } else {
974 reference.check_equivalence(other);
975 true
976 }
977}