From 347b9b24becff6f940f1536b83dbb94764fa5d8b Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Mon, 11 Dec 2023 19:42:23 +0100 Subject: [PATCH] Scheduler-test: complete and integrate computational load This basically completes the Chain-Load framework; a simple Scheduler integration run with all relevant features can now be demonstrated. --- tests/32scheduler.tests | 2 +- tests/vault/gear/test-chain-load-test.cpp | 93 +++--- tests/vault/gear/test-chain-load.hpp | 163 +++++++--- wiki/thinkPad.ichthyo.mm | 357 +++++++++++++++------- 4 files changed, 408 insertions(+), 207 deletions(-) diff --git a/tests/32scheduler.tests b/tests/32scheduler.tests index c183fd181..196242c25 100644 --- a/tests/32scheduler.tests +++ b/tests/32scheduler.tests @@ -50,7 +50,7 @@ END -PLANNED "Synthetic Load for Test" TestChainLoad_test < -//using lib::time::Time; -//using lib::time::FSecs; using util::isnil; using util::isSameObject; -//using lib::test::randStr; -//using lib::test::randTime; using std::array; @@ -86,36 +78,31 @@ namespace test { showcase_StablePattern(); verify_computation_load(); verify_reseed_recalculate(); + verify_runtime_reference(); verify_scheduling_setup(); } - /** @test TODO demonstrate simple usage of the test-load - * @todo WIP 11/23 🔁 define ⟶ 🔁 implement + /** @test demonstrate simple usage of the test-load + * @todo WIP 11/23 ✔ define ⟶ ✔ implement */ void usageExample() { - auto anchor = RealClock::now(); - auto offset = [&](Time when =RealClock::now()){ return _raw(when) - _raw(anchor); }; - auto testLoad = TestChainLoad{64} - .configureShape_simple_short_segments() + .configureShape_short_segments3_interleaved() .buildToplolgy(); -SHOW_EXPR(offset()) BlockFlowAlloc bFlow; EngineObserver watch; Scheduler scheduler{bFlow, watch}; -SHOW_EXPR(testLoad.getHash()) CHECK (testLoad.getHash() == 0xD2F292D864CF8086); -SHOW_EXPR(offset()) + testLoad.setupSchedule(scheduler) .launch_and_wait(); -SHOW_EXPR(offset()) -SHOW_EXPR(testLoad.getHash()) + CHECK (testLoad.getHash() == 0xD2F292D864CF8086); } @@ -691,6 +678,7 @@ SHOW_EXPR(testLoad.getHash()) CHECK (stat.indicators[STAT_EXIT].pL == "0.48076923"_expect); // ½ exit per level + // »short_segments_interleaved« // Increased seed probability combined with overall seed value 0 ◁──── (crucial, other seeds produce larger graphs) // produces what seems to be the best stable repetition loop: // same shape as in preceding, yet interwoven by 2 steps @@ -864,8 +852,8 @@ SHOW_EXPR(testLoad.getHash()) - /** @test WIP verify calibration of a configurable computational load. - * @todo WIP 12/23 🔁 define ⟶ 🔁 implement + /** @test verify calibration of a configurable computational load. + * @todo WIP 12/23 ✔ define ⟶ ✔ implement */ void verify_computation_load() @@ -874,74 +862,51 @@ SHOW_EXPR(testLoad.getHash()) CHECK (cpuLoad.timeBase == 100us); double micros = cpuLoad.invoke(); -SHOW_EXPR(micros) CHECK (micros < 2000); CHECK (micros > 2); cpuLoad.calibrate(); micros = cpuLoad.invoke(); -SHOW_EXPR(micros) CHECK (micros < 133); CHECK (micros > 80); micros = cpuLoad.benchmark(); -SHOW_EXPR(micros) CHECK (micros < 110); CHECK (micros > 90); cpuLoad.useAllocation = true; -SHOW_EXPR(cpuLoad.useAllocation) micros = cpuLoad.invoke(); -SHOW_EXPR(micros) CHECK (micros < 133); CHECK (micros > 80); micros = cpuLoad.benchmark(); -SHOW_EXPR(micros) CHECK (micros < 110); CHECK (micros > 90); cpuLoad.timeBase = 1ms; cpuLoad.sizeBase *= 100; -SHOW_EXPR(cpuLoad.sizeBase) - - cpuLoad.useAllocation = false; - micros = cpuLoad.benchmark(); -SHOW_EXPR(micros) - cpuLoad.useAllocation = true; - micros = cpuLoad.benchmark(); -SHOW_EXPR(micros) - cpuLoad.calibrate(); - - cpuLoad.useAllocation = false; - micros = cpuLoad.benchmark(); -SHOW_EXPR(micros) - cpuLoad.useAllocation = true; - micros = cpuLoad.benchmark(); -SHOW_EXPR(micros) -SHOW_EXPR(5) + cpuLoad.useAllocation = false; + micros = cpuLoad.invoke(); + CHECK (micros > 900); micros = cpuLoad.invoke(5); -SHOW_EXPR(micros) + CHECK (micros > 4600); + micros = cpuLoad.invoke(10); + CHECK (micros > 9500); + micros = cpuLoad.invoke(100); + CHECK (micros > 95000); + cpuLoad.useAllocation = true; + micros = cpuLoad.invoke(); + CHECK (micros > 900); micros = cpuLoad.invoke(5); -SHOW_EXPR(micros) -SHOW_EXPR(10) - cpuLoad.useAllocation = false; + CHECK (micros > 4600); micros = cpuLoad.invoke(10); -SHOW_EXPR(micros) - cpuLoad.useAllocation = true; - micros = cpuLoad.invoke(10); -SHOW_EXPR(micros) -SHOW_EXPR(100) - cpuLoad.useAllocation = false; + CHECK (micros > 9500); micros = cpuLoad.invoke(100); -SHOW_EXPR(micros) - cpuLoad.useAllocation = true; - micros = cpuLoad.invoke(100); -SHOW_EXPR(micros) + CHECK (micros > 95000); } @@ -1059,6 +1024,18 @@ SHOW_EXPR(micros) + /** @test TODO compute synchronous execution time for reference + * @todo WIP 12/23 🔁 define ⟶ 🔁 implement + */ + void + verify_runtime_reference() + { + + } + + + + /** @test setup for running a chain-load as scheduled task * - running an isolated Node recalculation * - dispatch of this recalculation packaged as render job diff --git a/tests/vault/gear/test-chain-load.hpp b/tests/vault/gear/test-chain-load.hpp index 43f113b43..2df7508f8 100644 --- a/tests/vault/gear/test-chain-load.hpp +++ b/tests/vault/gear/test-chain-load.hpp @@ -40,8 +40,8 @@ ** pass over all nodes; yet alternatively each node can be _scheduled_ as an individual ** computation job, which obviously requires that it's predecessor nodes have already ** been computed, otherwise the resulting hash will not match up with expectation. - ** If significant per-node computation time is required, the hashing can be - ** arbitrarily repeated at each node. + ** If significant per-node computation time is required, an artificial computational + ** load can be generated, controlled by a weight setting computed for each node. ** ** The topology of connectivity is generated randomly, yet completely deterministic through ** configurable _control functions_ driven by each node's (hash)value. This way, each node @@ -54,6 +54,11 @@ ** controlling the topology can be configured using the lib::RandomDraw component, allowing ** either just to set a fixed probability or to define elaborate dynamic configurations ** based on the graph height or node connectivity properties. + ** - expansionRule: controls forking of the graph behind the current node + ** - reductionRule: controls joining of the graph into a combining successor node + ** - seedingRule: controls injection of new start nodes in the middle of the graph + ** - pruningRule: controls insertion of exit nodes to cut-off some chain immediately + ** - weightRule: controls assignment of a Node::weight to command the ComputationalLoad ** ** ## Usage ** A TestChainLoad instance is created with predetermined maximum fan factor and a fixed @@ -70,6 +75,9 @@ ** and showing predecessor -> successor connectivity. Seed nodes are distinguished by ** circular shape. ** + ** Moreover, Statistics can be computed over the generated graph, allowing to draw some + ** conclusions regarding node distribution and connectivity. + ** ** @see TestChainLoad_test ** @see SchedulerStress_test ** @see random-draw.hpp @@ -83,17 +91,12 @@ #include "vault/common.hpp" #include "lib/test/transiently.hpp" -//#include "lib/hash-value.h" #include "vault/gear/job.h" #include "vault/gear/scheduler.hpp" #include "vault/gear/special-job-fun.hpp" -//#include "vault/gear/nop-job-functor.hpp" #include "lib/uninitialised-storage.hpp" #include "lib/test/microbenchmark.hpp" #include "lib/time/timevalue.hpp" -//#include "lib/meta/variadic-helper.hpp" -//#include "lib/meta/function.hpp" -//#include "lib/wrapper.hpp" #include "lib/time/quantiser.hpp" #include "lib/iter-explorer.hpp" #include "lib/format-string.hpp" @@ -117,17 +120,6 @@ namespace vault{ namespace gear { namespace test { - using std::string; - using std::function; - using lib::time::Time; - using lib::time::TimeValue; - using lib::time::FrameRate; - using lib::time::Duration; - using lib::test::benchmarkTime; - using lib::test::microBenchmark; -// using lib::time::FSecs; -// using lib::time::Offset; -// using lib::meta::RebindVariadic; using util::_Fmt; using util::min; using util::max; @@ -136,15 +128,22 @@ namespace test { using util::unConst; using util::toString; using util::showHashLSB; - using lib::meta::_FunRet; + using lib::time::Time; + using lib::time::TimeValue; + using lib::time::FrameRate; + using lib::time::Duration; + using lib::test::benchmarkTime; + using lib::test::microBenchmark; using lib::test::Transiently; + using lib::meta::_FunRet; + using std::string; + using std::function; using std::make_pair; using std::forward; using std::string; using std::swap; using std::move; -// using boost::hash_combine; using std::chrono_literals::operator ""s; namespace err = lumiera::error; @@ -160,9 +159,12 @@ namespace test { const size_t DEFAULT_CHUNKSIZE = 64; ///< number of computation jobs to prepare in each planning round const size_t LOAD_BENCHMARK_RUNS = 500; ///< repetition count for calibration benchmark for ComputationalLoad const double LOAD_SPEED_BASELINE = 100; ///< initial assumption for calculation speed (without calibration) + const microseconds LOAD_DEFAULT_TIME = 100us; ///< default time delay produced by ComputationalLoad at `Node.weight==1` + const size_t LOAD_DEFAULT_MEM_SIZE = 1000; ///< default allocation base size used if ComputationalLoad.useAllocation const microseconds PLANNING_TIME_PER_NODE = 80us; ///< time budget to reserve for each node to be planned and scheduled } + struct Statistic; @@ -405,6 +407,7 @@ namespace test { /** Abbreviation for starting rules */ static Rule rule() { return Rule(); } + static Rule value(size_t v) { return Rule().fixedVal(v); } static Rule rule_atStart (uint v) @@ -449,13 +452,33 @@ namespace test { } + /** preconfigured topology: isolated simple 2-step chains */ + TestChainLoad&& + configureShape_short_chains2() + { + pruningRule(rule().probability(0.8)); + weightRule(value(1)); + return move(*this); + } + + /** preconfigured topology: simple 3-step chains, starting interleaved */ + TestChainLoad&& + configureShape_short_chains3_interleaved() + { + pruningRule(rule().probability(0.6)); + seedingRule(rule_atStart(1)); + weightRule(value(1)); + return move(*this); + } + /** preconfigured topology: simple interwoven 3-step graph segments */ TestChainLoad&& - configureShape_simple_short_segments() + configureShape_short_segments3_interleaved() { seedingRule(rule().probability(0.8).maxVal(1)); reductionRule(rule().probability(0.75).maxVal(3)); pruningRule(rule_atJoin(1)); + weightRule(value(1)); return move(*this); } @@ -1077,6 +1100,23 @@ namespace test { /** * A calibratable CPU load to be invoked from a node job functor. + * Two distinct methods for load generation are provided + * - tight loop with arithmetics in register + * - repeatedly accessing and adding memory marked as `volatile` + * The `timeBase` multiplied with the given `scaleStep´ determines + * the actual run time. When using the _memory method_ (`useAllocation`), + * a heap block of `staleStep*sizeBase` is used, and the number of + * repetitions is chosen such as to match the given timing goal. + * @note since performance depends on the platform, it is mandatory + * to invoke the [self calibration](\ref ComputationalLoad::calibrate) + * at least once prior to use. Performing the calibration with default + * base settings is acceptable, since mostly the overall expense is + * growing linearly; obviously the calibration is more precisely + * however when using the actual `timeBase` and `sizeBase` of + * the intended usage. The calibration watches processing + * speed in a microbenchmark with `LOAD_BENCHMARK_RUNS` + * repetitions; the result is stored in a static + * variable and can thus be reused. */ class ComputationalLoad { @@ -1093,43 +1133,49 @@ namespace test { } public: - microseconds timeBase = 100us; - size_t sizeBase = 1000; + microseconds timeBase = LOAD_DEFAULT_TIME; + size_t sizeBase = LOAD_DEFAULT_MEM_SIZE; bool useAllocation = false; + /** cause a delay by computational load */ double invoke (uint scaleStep =1) { - if (scaleStep == 0) return 0; + if (scaleStep == 0 or timeBase < 1us) + return 0; // disabled return useAllocation? benchmarkTime ([this,scaleStep]{ causeMemProcessLoad (scaleStep); }) : benchmarkTime ([this,scaleStep]{ causeComputationLoad(scaleStep); }); } + /** @return averaged runtime in current configuration */ double benchmark (uint scaleStep =1) { return microBenchmark ([&]{ invoke(scaleStep);} ,LOAD_BENCHMARK_RUNS) - .first; + .first; // ∅ runtime in µs } void calibrate() { -cout<<">CAL: speed="<"<"<"<invoke (target.weight); target.calculate(); } @@ -1363,7 +1412,7 @@ cout<<_Fmt{"\n!◆! %s: calc(i=%d, lev:%d)"} % markThread() % nodeIdx % level << { size_t reachedLevel{0}; size_t targetNodeIDX = decodeNodeID (param.invoKey); -cout<<_Fmt{"\n!◆!plan...to:%d%19t|curr=%d (max:%d)"} % targetNodeIDX % currIdx_ % maxCnt_<level < schedule_; FrameRate levelSpeed_{1, Duration{_uTicks(1ms)}}; - uint loadFactor_{2}; + uint blockLoadFactor_{2}; size_t chunkSize_{DEFAULT_CHUNKSIZE}; TimeVar startTime_{Time::ANYTIME}; microseconds preRoll_{guessPlanningPreroll (chunkSize_)}; @@ -1422,6 +1471,7 @@ cout<<_Fmt{"%16t|n.(%d,lev:%d)"} % currIdx_ % n->level < signalDone_{}; + std::unique_ptr compuLoad_; std::unique_ptr> calcFunctor_; std::unique_ptr> planFunctor_; @@ -1476,6 +1526,7 @@ cout <<"--> reschedule to ..."<maybeCalibrate(); startTime_ = anchorStartTime(); scheduler_.seedCalcStream (planningJob(firstChunkEndNode) ,manID_ @@ -1487,7 +1538,8 @@ cout <<"+++ "<{chainLoad_.nodes_[0]}} + , compuLoad_{new ComputationalLoad} + , calcFunctor_{new RandomChainCalcFunctor{chainLoad_.nodes_[0], compuLoad_.get()}} , planFunctor_{new RandomChainPlanFunctor{chainLoad_.nodes_[0], chainLoad_.numNodes_ ,[this](size_t i, size_t l){ disposeStep(i,l); } ,[this](auto* p, auto* s) { setDependency(p,s);} @@ -1515,7 +1567,7 @@ cout <<"+++ "<timeBase = timeBase; + } + + ScheduleCtx&& + deactivateLoad() + { + compuLoad_->timeBase = 0us; + } + + ScheduleCtx&& + withLoadMem (size_t sizeBase =LOAD_DEFAULT_MEM_SIZE) + { + if (not sizeBase) + { + compuLoad_->sizeBase = LOAD_DEFAULT_MEM_SIZE; + compuLoad_->useAllocation =false; + } + else + { + compuLoad_->sizeBase = sizeBase; + compuLoad_->useAllocation =true; + } + } + private: /** push away any existing wait state and attach new clean state */ std::future @@ -1609,7 +1688,7 @@ cout<<"ANCHOR="+relT(ank)+" preRoll="+util::toString(_raw(_uTicks(preRoll_)))< - - + + @@ -96396,8 +96396,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -96767,14 +96767,14 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + - - - + + + - - + + @@ -96813,15 +96813,23 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + + + + + + + + + @@ -99442,8 +99450,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -99483,8 +99491,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -99536,8 +99544,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -99567,37 +99575,49 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

also: beides

- -
+
- - - +

legt die Eichungs-Konstanten statisch ab

- -
+
+ + + + + + + + + + + + + + + + + + @@ -99606,7 +99626,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -99626,8 +99646,8 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + @@ -99636,9 +99656,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

schwierig... @@ -99653,8 +99671,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
Stelle schon mal fest: der Einzel-Lauf streut ganz deutlich. Und Cache-Effekte könnten auch noch ein gewisses Problem darstellen (wenngleich auch der vorangegangene Test grundsätzlich den Code schon vorgewärmt  hat. Also setze mal  ±30% für den Einzeltest an, aber nur ±10% für den Benchmark

- -
+
@@ -99664,9 +99681,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

bei.. @@ -99678,15 +99693,14 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
sizeBase = 100000 ⟼ +10-20%

- -
+
- - + + @@ -99696,22 +99710,70 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - +

repetitions/µs

- -
+
- + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + @@ -99739,7 +99801,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -100014,7 +100076,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -100055,6 +100117,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + @@ -100916,7 +100985,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -100996,21 +101065,27 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + + + + + + + + + + - - - +

und zwar der Umstand, daß die Nodes innerhalb eines Levels stets voneinander unabhängig sind; deshalb sind zumindest diese Nodes komplett parallelisierbar

- -
+
@@ -101018,9 +101093,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + @@ -101057,7 +101132,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -101251,17 +101326,20 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + + + - - - + + + + @@ -101594,7 +101672,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -101614,7 +101692,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -101647,7 +101725,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -101677,12 +101755,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - + + - + + @@ -101698,7 +101777,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -101885,12 +101964,13 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - + + + + - + @@ -102024,6 +102104,17 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + +

+ Fazit: durch Rückbau des Gate-re-Scheduling behoben +

+ +
+ +
@@ -102033,7 +102124,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -102117,7 +102208,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -102233,10 +102324,11 @@ Date:   Thu Apr 20 18:53:17 2023 +0200

+
- + @@ -102376,7 +102468,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -102487,27 +102579,9 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- - - - - - + - - - - - - - - - - - - - @@ -102692,8 +102766,16 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + - + + @@ -102739,7 +102821,7 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
- + @@ -102760,6 +102842,61 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + +
    +
  • + die eigentlichen Work-Jobs praktisch nix zu tun haben +
  • +
  • + mehr als 3 Worker um das Grooming-Token konkurrieren (bei 3 Workern ist ∅ keine Verzögerung verststellbar, auch wenn sie ziemlich viel herumeiern) +
  • +
  • + nacheinander mehrere Jobs geprüft werden, die dann jeweils nicht ausführbar sind +
  • +
+

+ Der zuletzt genannte Punkt ist inhärent problematisch — es würde sich nicht lohnen, das Grooming-Token zu droppen, da der bloße Check weniger als 1µs kostet, der Test auf das Grooming-Token danach aber wieder 10-20µs. Unter Contention allerdings kann sich eine solche Prüf-Kette auf 100-200µs ausdehnen, und während der Zeit passiert sonst praktisch nix. +

+ +
+ +
+ + + + + +
+ + + + + + + +

+ unter »Zugriffs-Phase« verstehe ich die Zeit, während der die Queues modifiziert werden müssen, was nur exklusiv von einem Thread gemacht werden kann (erst danach könnte dieser mit eigentlicher Arbeit „weggehen“). Das Problem ist nun: wenn wir im Rückstau sind, dann werden alle Threads an die Head-Time geschickt. Dort drehen sie dann am Rad und machen dadurch alles viel langsamer +

+ +
+ +
+ + + + +

+ ...wir reden hier über Abläufe in der Größenordnung der »Grund-Unschärfe« (100µs); und das Problem tritt nur auf, wenn die eigentliche Aktion in der gleichen (kurzen) Zeit erledigt sein könnte. Wenn dagegen 500µs gearbeitet werden müßte, sollte sich das Verhalten (theoretisch) von selbst entspannen +

+ +
+ +
@@ -102794,6 +102931,14 @@ Date:   Thu Apr 20 18:53:17 2023 +0200
+ + + + + + + +