//+------------------------------------------------------------------+ //| Warrior_EA | //| CPU thread-pool compute kernels, semantics mirror | //| AI\Network.cl / WarriorDML.cpp exactly (see those files | //| for the reference derivation of each kernel). | //+------------------------------------------------------------------+ #include "WarriorCPU.h" #define NOMINMAX #include #include #include #include #include #include #include #include #include #include #include #include namespace { // Fault isolation boundary: a hardware exception (bad pointer, misaligned // SIMD access, etc.) or a C++ exception raised while running a kernel must // never be allowed to unwind past this DLL, because MQL5/#import has no // concept of an exception - it would fall straight through to the OS // unhandled-exception path and kill the whole terminal process (this is // exactly how the 0xc0000409 overnight crash happened). Every kernel // dispatch and the pool-startup call are routed through these two guards // instead, so a fault degrades to "this call failed, CPU_GetLastError() // explains why" - the caller (CNet on the MQL5 side) can retry/reinit // instead of the whole terminal dying. // // Split into their own functions because MSVC forbids mixing __try/__except // with C++ try/catch in the same function (C2712/C2713) - the C++ exception // guard and the SEH guard each need their own stack frame. bool CallFnNoThrow(const std::function &fn, int begin, int end) { try { fn(begin, end); return true; } catch(...) { return false; } } bool SehCallFn(const std::function &fn, int begin, int end) { __try { return CallFnNoThrow(fn, begin, end); } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } } bool SehStartPool(class ThreadPool &pool, int threads); //+------------------------------------------------------------------+ //| Minimal persistent thread pool with a parallel_for helper. Work | //| is split into contiguous chunks (one per worker) so cache lines | //| stay reasonably local; a countdown latch signals completion. | //| Owned exclusively by one CpuContext - never shared across | //| contexts, so no cross-instance coordination (grow/refcount) is | //| needed here at all, just plain start-once/stop-once lifecycle. | //+------------------------------------------------------------------+ class ThreadPool { public: void Start(int threads) { Stop(); if(threads <= 0) threads = (int)std::thread::hardware_concurrency(); if(threads <= 0) threads = 1; { std::lock_guard lock(m_mutex); m_stop = false; } for(int i = 0; i < threads; i++) m_workers.emplace_back([this]{ WorkerLoop(); }); m_workerCount.store(threads); } void Stop() { if(m_workers.empty()) return; { std::lock_guard lock(m_mutex); m_stop = true; } m_cv.notify_all(); // Bounded wait per worker instead of a plain join(): std::thread has no native timed join, so // each worker's join() runs on a throwaway "joiner" thread and this thread waits on it via a // future with a timeout instead of blocking directly. CPU_Shutdown() calls Stop() while holding // this context's mutex (see its declaration comment) - if MQL5's DLL-call watchdog force-kills // the calling thread while it's stuck in an unbounded join(), that mutex would stay locked // FOREVER, poisoning every future call against THIS context. Because each context is private to // one CDirectMLMy instance (no global/shared state - see CpuContext), that poisoning can no // longer take out every other chart's calls into this DLL the way a process-wide singleton // would; it only strands the one instance that hit it. Ownership of each worker's std::thread is // moved into the joiner's closure, so if we give up and detach the joiner on timeout, the // now-empty (moved-from, non-joinable) entry left behind in m_workers is safe to destroy - the // joiner keeps running in the background and joins the real worker whenever it actually exits. const auto timeout = std::chrono::seconds(10); for(auto &t : m_workers) { if(!t.joinable()) continue; std::promise done; std::future fut = done.get_future(); std::thread joiner([owned = std::move(t), prom = std::move(done)]() mutable { owned.join(); prom.set_value(); }); if(fut.wait_for(timeout) == std::future_status::timeout) joiner.detach(); else joiner.join(); } m_workers.clear(); m_workerCount.store(0); { std::lock_guard lock(m_mutex); std::queue> empty; std::swap(m_tasks, empty); m_stop = false; } } int ThreadCount() const { return m_workerCount.load(); } ~ThreadPool() { Stop(); } // Runs fn(begin,end) once per chunk, chunks covering [0,count) contiguously, // one chunk per worker thread; blocks until every chunk has completed. // Returns false if any chunk faulted (see SehCallFn) - the caller's buffer // is then left partially/not updated by that chunk, so the whole call must // be treated as failed rather than silently reporting success. // // Takes fn BY VALUE and heap-allocates every piece of state the enqueued // tasks touch (via shared_ptr), rather than referencing this function's own // stack locals. Reason: CpuContext's declaration comment already documents // that MQL5's terminal can forcibly kill the DLL-calling thread mid-call if // it decides the call is hung. When that happens here, the ThreadPool's // worker threads are NOT killed with it - they are independent OS threads // that keep running and, previously, kept writing into `remaining`/`allOk`/ // `doneMutex`/`doneCv` (and calling through `fn`) after the stack frame that // owned them had already been torn down. That is a use-after-free into a // dead stack - undefined behavior that reliably corrupts the process rather // than just failing this one call, which is exactly the class of crash this // was supposed to be immune to (see SehCallFn). Every kernel dispatch site // below also passes its lambda as [=] rather than [&] for the same reason - // a by-reference capture of that call's own local pointers/ints would be // just as dangling once its frame is gone. bool ParallelFor(int count, std::function fn) { if(count <= 0) return true; // Below this, the heap allocs + mutex/condvar handoff below cost more than just running the // whole range inline on the calling thread - this network's layers are small (tens to low // hundreds of neurons), so nearly every dispatch was paying full thread-pool overhead for a // sliver of work per thread. Threshold is deliberately conservative (only skips clearly-not- // worth-it cases); raise it further if profiling shows larger layers still dominated by overhead. const int kInlineThreshold = 512; int workers = std::max(1, m_workerCount.load()); int chunks = std::min(workers, count); if(chunks <= 1 || count < kInlineThreshold) return SehCallFn(fn, 0, count); struct State { std::atomic remaining; std::atomic allOk{true}; std::mutex doneMutex; std::condition_variable doneCv; explicit State(int n) : remaining(n) {} }; auto state = std::make_shared(chunks); auto fnPtr = std::make_shared>(std::move(fn)); int base = count / chunks; int extra = count % chunks; int start = 0; for(int c = 0; c < chunks; c++) { int len = base + (c < extra ? 1 : 0); int begin = start; int end = start + len; start = end; Enqueue([state, fnPtr, begin, end] { // SehCallFn swallows both hardware faults and C++ exceptions raised // inside the kernel; either would otherwise escape this std::thread // with no handler and kill the whole terminal process. `remaining` // must still be decremented on failure or every other in-flight // ParallelFor caller waiting on doneCv would deadlock forever. if(!SehCallFn(*fnPtr, begin, end)) state->allOk.store(false); if(state->remaining.fetch_sub(1) == 1) { std::lock_guard lock(state->doneMutex); state->doneCv.notify_one(); } }); } std::unique_lock lock(state->doneMutex); state->doneCv.wait(lock, [state]{ return state->remaining.load() == 0; }); return state->allOk.load(); } private: void Enqueue(std::function job) { { std::lock_guard lock(m_mutex); m_tasks.push(std::move(job)); } m_cv.notify_one(); } void WorkerLoop() { for(;;) { std::function job; { std::unique_lock lock(m_mutex); m_cv.wait(lock, [this]{ return m_stop || !m_tasks.empty(); }); if(m_stop && m_tasks.empty()) return; job = std::move(m_tasks.front()); m_tasks.pop(); } job(); } } std::vector m_workers; std::queue> m_tasks; std::mutex m_mutex; std::condition_variable m_cv; bool m_stop = false; std::atomic m_workerCount{0}; }; bool SehStartPool(ThreadPool &pool, int threads) { __try { pool.Start(threads); return true; } __except(EXCEPTION_EXECUTE_HANDLER) { return false; } } struct Buffer { std::vector data; bool inUse = false; }; //+------------------------------------------------------------------+ //| Everything one CDirectMLMy/CNet instance needs, heap-allocated by | //| CPU_Init() and handed back as an opaque CpuHandle. No part of | //| this DLL keeps any global or static mutable state - every export | //| below (besides CPU_Init/CPU_GetHardwareConcurrency) operates only | //| on the CpuContext its caller passes in, so a fault or a wedged | //| call (e.g. the mutex-poisoning scenario in ThreadPool::Stop's | //| comment) can only ever strand its own context, never another | //| instance's, and the DLL can be loaded/used by any number of | //| instances or threads in parallel with zero cross-talk. | //+------------------------------------------------------------------+ struct CpuContext { // Guards buffers/lastError bookkeeping ONLY - never the parallel dispatch/wait itself. Every // kernel below locks this mutex just long enough to validate handles and extract raw buffer // pointers, then releases it BEFORE calling pool.ParallelFor(), which blocks (potentially for a // while, especially under load) waiting for every worker chunk to finish. MQL5's terminal // enforces a hang-watchdog on EA execution: if a #import DLL call ever runs long enough to trip // it, the terminal can forcibly terminate the calling thread mid-call. If that termination // happened while this thread held `mutex` across a ParallelFor wait, the lock_guard's destructor // would never run and `mutex` would stay locked FOREVER - but since it belongs to this context // alone, that only poisons future calls against this one instance, not any other chart's. std::mutex mutex; ThreadPool pool; int lastError = 0; std::vector buffers; }; int AllocBufferSlot(CpuContext *ctx) { for(size_t i = 0; i < ctx->buffers.size(); i++) if(!ctx->buffers[i].inUse) return (int)i; ctx->buffers.push_back(Buffer()); return (int)ctx->buffers.size() - 1; } bool ValidHandle(CpuContext *ctx, int h) { return h >= 0 && (size_t)h < ctx->buffers.size() && ctx->buffers[h].inUse; } inline size_t Size(CpuContext *ctx, int h) { return ctx->buffers[h].data.size(); } // A caller-supplied count/shape argument mismatched against the actual buffer // size would index past the end of a std::vector - undefined behavior that // (unlike an overrun into a GPU UAV buffer) reliably crashes the whole host // process. Every kernel below validates the exact index range it is about to // touch against the real buffer sizes first and bails out to an MQL5-visible // "false" return instead. #define REQUIRE(ctx, cond) do { if(!(cond)) { (ctx)->lastError = 100; return 0; } } while(0) // Mirrors AI\Network.cl's MAX_WEIGHT clamp, applied to every weight-update kernel below. Without it a // gradient spike (e.g. from class-balance oversampling replaying the same rare-class bar several times // in a row) can drive a weight to +-Infinity; the next Adam step then divides Infinity by Infinity // (mt/vt both Inf), producing NaN, which propagates through every FeedForward sum that touches it and // never recovers (Adam(NaN) is always NaN). That silently freezes the whole network at NaN output - // manifesting as every bar classifying to whatever the "can't decide" default is, since NaN comparisons // are always false. // Tightened from 1.0e6 - that ceiling was so loose it never actually engaged before training had // already gone unstable (real collapses were happening at weight magnitudes several orders of // magnitude below it). 100.0 matches the equivalent clamp in Dmitriy Gizlyk's reference NeuroNet.mqh // engine (references\MQL5\Experts\NeuroNet_DNG\NeuroNet.mqh). const double MAX_WEIGHT = 100.0; inline double ClampWeight(double w) { return std::max(-MAX_WEIGHT, std::min(MAX_WEIGHT, w)); } // Floor on the magnitude of a saturated tanh/sigmoid unit's activation derivative. Without this, a // neuron pinned near its activation extremes (output near -1/0/1) produces a near-zero derivative, // zeroing that neuron's entire backprop gradient contribution regardless of how wrong its output is - // it can then never receive a corrective signal to unstick it. 1e-4 matches the equivalent floor in // Dmitriy Gizlyk's reference NeuroNet.mqh/NeuroNet.cl engine. const double MIN_ACTIVATION_DERIVATIVE = 1.0e-4; // Decoupled (AdamW-style) weight decay applied inside every Adam kernel below, on top of the // MAX_WEIGHT clamp above. MAX_WEIGHT only stops outright +-Infinity/NaN blowups; it does nothing to // stop weights slowly, unboundedly growing over hundreds of training eras on a fixed, heavily // class-balance-oversampled dataset (the same rare-class bars replayed up to 5x every single era, // forever, with no shuffling - see Train()'s reps loop). That slow growth is what was producing the // multi-hour climb-to-90%+-then-collapse-to-single-digits cycles: OOS accuracy would improve for // dozens of eras as weights fit the data, then the accumulated growth would push the network into an // unstable regime (large IS error spike alongside the OOS collapse) before Adam re-settled and the // climb started over. Every Adam step pulls each weight a small fraction of the way back toward // zero, capping how large it can drift regardless of how many eras keep pushing it the same // direction. 0.001, NOT the 0.01 Loshchilov & Hutter default - see AI\Network.mqh's WEIGHT_DECAY // comment for the full derivation: decay is applied per SAMPLE here (~20k+ online steps/era), and // 0.01 was observed to grind discriminative weights below the calibration-capped class-prior // offsets (per-bar logit spread decaying monotonically until argmax degenerated to constant- // Neutral), while 0.001 still bounds long-run growth. Keep in sync across all four backends. // UPDATE: weight decay alone did not fully stop the cycles - it targets slow unbounded drift, but the // actual collapses turned out to be sudden, violent overshoot events (OOS accuracy falling BELOW the // 33% random-guess floor for 3-class within ~20 eras, IS error spiking to 0.85+ - the model becoming // confidently wrong, not just uncalibrated). That's a single/few-step Adam blowup, most likely // triggered by the class-balance oversampling replaying the same rare-class bar up to 5x back-to-back // every era (see Train()'s reps loop) - 5 identical consecutive gradients build artificially strong, // correlated momentum (mt) that can overshoot badly once the next, different bar arrives. MAX_WEIGHT_DELTA // below clips the actual per-step update, not just the resulting weight, which directly bounds how much // damage any single overshoot step (whatever triggers it) can do. const double WEIGHT_DECAY = 0.001; // Per-step update clip - see the UPDATE note above. A healthy Adam step is normally O(lt) (~1e-4 to // 1e-3 here); 0.1 is generous headroom for legitimate fast learning while hard-stopping the // multi-order-of-magnitude spikes that were whipping OOS accuracy from 90%+ down to single digits in // a couple dozen eras. Applied to `delta` BEFORE it's added to the weight, unlike MAX_WEIGHT (which // only clamps the post-update weight value and is far too loose - 1e6 - to prevent this). const double MAX_WEIGHT_DELTA = 0.1; inline double ClampDelta(double d) { return std::max(-MAX_WEIGHT_DELTA, std::min(MAX_WEIGHT_DELTA, d)); } inline double Activation(double sum, int activation) { if(activation == 0) return tanh(sum); if(activation == 1) return 1.0 / (1.0 + exp(-std::max(-50.0, std::min(50.0, sum)))); if(activation == 2 && sum < 0) return sum * 0.01; return sum; } } // namespace //+------------------------------------------------------------------+ //| Lifecycle | //+------------------------------------------------------------------+ WARRIORCPU_API CpuHandle __stdcall CPU_Init(int threads) { CpuContext *ctx = new CpuContext(); if(!SehStartPool(ctx->pool, threads)) { delete ctx; // hardware fault during pool startup - see SehStartPool return 0; } return reinterpret_cast(ctx); } // Stateless query of the machine's true hardware concurrency - touches no // context at all, so callers can size a CPU_Init() request correctly without // a probe-then-discard Init/GetThreadCount/Shutdown roundtrip. WARRIORCPU_API int __stdcall CPU_GetHardwareConcurrency() { int n = (int)std::thread::hardware_concurrency(); return n > 0 ? n : 1; } WARRIORCPU_API void __stdcall CPU_Shutdown(CpuHandle handle) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return; ctx->pool.Stop(); delete ctx; } WARRIORCPU_API int __stdcall CPU_GetLastError(CpuHandle handle) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; std::lock_guard lock(ctx->mutex); return ctx->lastError; } WARRIORCPU_API int __stdcall CPU_GetThreadCount(CpuHandle handle) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; return ctx->pool.ThreadCount(); } //+------------------------------------------------------------------+ //| Buffers | //+------------------------------------------------------------------+ WARRIORCPU_API int __stdcall CPU_BufferCreate(CpuHandle handle, int elementCount) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx || elementCount <= 0) return -1; std::lock_guard lock(ctx->mutex); int slot = AllocBufferSlot(ctx); ctx->buffers[slot].data.assign((size_t)elementCount, 0.0); ctx->buffers[slot].inUse = true; return slot; } WARRIORCPU_API void __stdcall CPU_BufferFree(CpuHandle handle, int bufHandle) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return; std::lock_guard lock(ctx->mutex); if(bufHandle < 0 || (size_t)bufHandle >= ctx->buffers.size()) return; ctx->buffers[bufHandle] = Buffer(); } WARRIORCPU_API int __stdcall CPU_BufferWrite(CpuHandle handle, int bufHandle, const double *data, int count) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, bufHandle) || (size_t)count > ctx->buffers[bufHandle].data.size()) return 0; // A NaN/Inf slipping into a weight or gradient buffer doesn't crash anything - // it silently poisons every value it touches from then on (NaN propagates // through every arithmetic op), so training runs for hours producing garbage // with no error ever reported. Reject it at the boundary instead. for(int i = 0; i < count; i++) REQUIRE(ctx, std::isfinite(data[i])); std::copy(data, data + count, ctx->buffers[bufHandle].data.begin()); return 1; } WARRIORCPU_API int __stdcall CPU_BufferRead(CpuHandle handle, int bufHandle, double *data, int count) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, bufHandle) || (size_t)count > ctx->buffers[bufHandle].data.size()) return 0; std::copy(ctx->buffers[bufHandle].data.begin(), ctx->buffers[bufHandle].data.begin() + count, data); return 1; } //+------------------------------------------------------------------+ //| Dense feed-forward / gradient / weight-update kernels - mirror | //| AI\Network.cl's FeedForward/CaclOutputGradient/CaclHiddenGradient/| //| UpdateWeightsMomentum/UpdateWeightsAdam 1:1, parallelized over | //| the neuron (outer) dimension. | //+------------------------------------------------------------------+ WARRIORCPU_API int __stdcall CPU_FeedForward(CpuHandle handle, int wHandle, int iHandle, int oHandle, int inputs, int activation) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; int neurons; double *w, *in, *out; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, iHandle) || !ValidHandle(ctx, oHandle)) return 0; neurons = (int)Size(ctx, oHandle); REQUIRE(ctx, inputs >= 0 && neurons >= 0); REQUIRE(ctx, Size(ctx, iHandle) >= (size_t)inputs); REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)neurons * (inputs + 1)); w = ctx->buffers[wHandle].data.data(); in = ctx->buffers[iHandle].data.data(); out = ctx->buffers[oHandle].data.data(); } if(!ctx->pool.ParallelFor(neurons, [=](int begin, int end) { for(int i = begin; i < end; i++) { int shift = (inputs + 1) * i; double sum = 0.0; for(int k = 0; k < inputs; k++) sum += in[k] * w[shift + k]; sum += w[shift + inputs]; out[i] = Activation(sum, activation); } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_CalcOutputGradient(CpuHandle handle, int tHandle, int oHandle, int igHandle, int activation, int count) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *t, *o, *ig; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, tHandle) || !ValidHandle(ctx, oHandle) || !ValidHandle(ctx, igHandle)) return 0; REQUIRE(ctx, count >= 0 && Size(ctx, tHandle) >= (size_t)count && Size(ctx, oHandle) >= (size_t)count && Size(ctx, igHandle) >= (size_t)count); t = ctx->buffers[tHandle].data.data(); o = ctx->buffers[oHandle].data.data(); ig = ctx->buffers[igHandle].data.data(); } if(!ctx->pool.ParallelFor(count, [=](int begin, int end) { for(int i = begin; i < end; i++) { double out_v = o[i]; double temp = 0.0; if(activation == 0) { // Deliberately NOT multiplied by the tanh derivative (1-out^2): // that factor vanishes as out_v approaches +-1, which is exactly // where a TANH output neuron needs to converge for a +-1 target // (e.g. the buy/sell extremes of a single-neuron regression // head), stalling training right when it matters most. Using the // raw (target-out) error is the same fix as pairing sigmoid with // cross-entropy - the saturating derivative doesn't re-damp the // already-correct error signal. temp = std::max(-1.0, std::min(1.0, t[i])) - out_v; } else if(activation == 1) { // Also deliberately NOT multiplied by the sigmoid derivative out_v*(1-out_v) - // same reasoning as activation==0 above. This is the classification output layer // (3 neurons, one-hot 0/1 targets); (target-out) unscaled is exactly the // sigmoid+binary-cross-entropy gradient (the loss derivative and the sigmoid // derivative algebraically cancel), whereas re-multiplying by out_v*(1-out_v) is // the MSE-with-sigmoid formula, which damps toward zero as out_v approaches 0 or 1 // - exactly where a 0/1-target neuron needs to converge. With this damping (plus // the 0.00000001 underflow guard at the saturated extremes), the classification // output layer's effective learning rate was throttled to a small fraction of the // regression path's, observed in practice as all 3 output neurons converging to // (and never escaping) an identical value - softmax of three equal logits is // always exactly 1/3 each, with ties resolved to index 0 ("Buy" wins by default). temp = std::max(0.0, std::min(1.0, t[i])) - out_v; } else { // NONE (raw logits - the 3-class softmax classification output layer) and PRELU (2, // never actually used as an OUTPUT activation in this codebase) both fall here. // Identity/NONE has derivative 1 everywhere, so the plain (target-out) error passes // through unscaled - matching CNeuron::calcOutputGradients()'s unconditional formula // on the MQL5-side un-accelerated fallback. Leaving this an unhandled else left temp // at 0.0, i.e. every softmax-classification output neuron got a permanently zero // gradient on this DLL - and since CPU_CalcHiddenGradient propagates backward FROM // the output layer's gradient, that silently froze the whole network (not just the // output layer) at its random initial weights whenever the CPU tier was active. temp = t[i] - out_v; } ig[i] = temp; } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_CalcHiddenGradient(CpuHandle handle, int wHandle, int gHandle, int oHandle, int igHandle, int outputs, int activation, int count) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; size_t wSize, oSize, gSize; double *w, *g, *o, *ig; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, gHandle) || !ValidHandle(ctx, oHandle) || !ValidHandle(ctx, igHandle)) return 0; REQUIRE(ctx, count >= 0 && outputs >= 0); REQUIRE(ctx, Size(ctx, igHandle) >= (size_t)count); // count is deliberately Neurons()+1 (an extra row for the bias gradient // slot - see Gradient's BufferInit(numNeurons+1,...)), but Output and // Weights only have Neurons() rows (no matching bias row), so the last // row's index legitimately runs past the end of both buffers. That's a // harmless OOB on a GPU UAV read (the result is never used downstream - // biases don't get a backprop weight); on CPU every read here is // bounds-checked and treated as 0 instead of rejecting the whole call. wSize = Size(ctx, wHandle); oSize = Size(ctx, oHandle); gSize = Size(ctx, gHandle); w = ctx->buffers[wHandle].data.data(); g = ctx->buffers[gHandle].data.data(); o = ctx->buffers[oHandle].data.data(); ig = ctx->buffers[igHandle].data.data(); } if(!ctx->pool.ParallelFor(count, [=](int begin, int end) { for(int i = begin; i < end; i++) { int shift = (outputs + 1) * i; double out_v = ((size_t)i < oSize) ? o[i] : 0.0; double sum = 0.0; for(int k = 0; k < outputs; k++) { size_t wi = (size_t)shift + k; if(wi < wSize && (size_t)k < gSize) sum += g[k] * w[wi]; } if(activation == 0) { sum = std::max(-1.0, std::min(1.0, sum + out_v)) - out_v; sum *= std::max(MIN_ACTIVATION_DERIVATIVE, 1.0 - out_v * out_v); } else if(activation == 1) { sum = std::max(0.0, std::min(1.0, sum + out_v)) - out_v; sum *= std::max(MIN_ACTIVATION_DERIVATIVE, out_v * (1.0 - out_v)); } else if(activation == 2 && out_v < 0.0) { // PReLU, param=0.01 - unbounded/non-saturating, no clamp/implied-target // reformulation needed (matches CPU_CalcHiddenGradientConv's activation==2 branch a // few functions below, and AI\Network.cl's CaclHiddenGradient case 2). This branch was // missing entirely, so every hidden PReLU layer (the default hidden-layer activation - // see ExpertSignalAIBase.mqh's HiddenLayerActivation()) had its backprop gradient left // completely unscaled on this backend whenever out_v was negative - a 100x overstated // gradient (1.0 instead of the correct 0.01) into that neuron's input weights, active // whenever the CPU DLL tier is in use (i.e. no OpenCL GPU available). sum *= 0.01; } ig[i] = sum; } })) { ctx->lastError = 201; return 0; } return 1; } // Bias weight (j == inputs) is intentionally never touched here, matching // the OpenCL/DirectML UpdateWeightsMomentum dispatch range (0..inputs-1). WARRIORCPU_API int __stdcall CPU_UpdateWeightsMomentum(CpuHandle handle, int wHandle, int gHandle, int iHandle, int dwHandle, int inputs, double learningRate, double momentum, int neurons) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *w, *g, *in, *dw; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, gHandle) || !ValidHandle(ctx, iHandle) || !ValidHandle(ctx, dwHandle)) return 0; REQUIRE(ctx, neurons >= 0 && inputs >= 0); REQUIRE(ctx, Size(ctx, gHandle) >= (size_t)neurons && Size(ctx, iHandle) >= (size_t)inputs); REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)neurons * (inputs + 1) && Size(ctx, dwHandle) >= (size_t)neurons * (inputs + 1)); w = ctx->buffers[wHandle].data.data(); g = ctx->buffers[gHandle].data.data(); in = ctx->buffers[iHandle].data.data(); dw = ctx->buffers[dwHandle].data.data(); } if(!ctx->pool.ParallelFor(neurons, [=](int begin, int end) { for(int i = begin; i < end; i++) for(int j = 0; j < inputs; j++) { int wi = i * (inputs + 1) + j; double delta = learningRate * g[i] * in[j] + momentum * dw[wi]; dw[wi] = delta; w[wi] = ClampWeight(w[wi] + delta); } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_UpdateWeightsAdam(CpuHandle handle, int wHandle, int gHandle, int iHandle, int mHandle, int vHandle, int inputs, double lt, double b1, double b2, int neurons) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *w, *g, *in, *m, *v; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, gHandle) || !ValidHandle(ctx, iHandle) || !ValidHandle(ctx, mHandle) || !ValidHandle(ctx, vHandle)) return 0; REQUIRE(ctx, neurons >= 0 && inputs >= 0); REQUIRE(ctx, Size(ctx, gHandle) >= (size_t)neurons && Size(ctx, iHandle) >= (size_t)inputs); REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)neurons * (inputs + 1) && Size(ctx, mHandle) >= (size_t)neurons * (inputs + 1) && Size(ctx, vHandle) >= (size_t)neurons * (inputs + 1)); w = ctx->buffers[wHandle].data.data(); g = ctx->buffers[gHandle].data.data(); in = ctx->buffers[iHandle].data.data(); m = ctx->buffers[mHandle].data.data(); v = ctx->buffers[vHandle].data.data(); } if(!ctx->pool.ParallelFor(neurons, [=](int begin, int end) { for(int i = begin; i < end; i++) for(int j = 0; j <= inputs; j++) { int wi = i * (inputs + 1) + j; double inp = (j < inputs) ? in[j] : 1.0; double grad = g[i] * inp; double mt = b1 * m[wi] + (1.0 - b1) * grad; double vt = sqrt(b2 * v[wi] + (1.0 - b2) * grad * grad); double delta = ClampDelta(lt * mt / (vt > 0.0 ? vt : lt * 10.0) - lt * WEIGHT_DECAY * w[wi]); // No sign-agreement gate (removed 2026-07): gating each step on agreement with the // CURRENT sample's gradient sign rectified the one-hot softmax-CCE stream - rare large // true-class positives, frequent small wrong-class negatives - into a permanent downward // ratchet on every output neuron, sinking all three logits into sigmoid saturation // together (the all-Neutral collapse; IS error frozen at sqrt(1/3)=0.58). The stale-step // overshoot it guarded against is covered by ClampDelta, AdamW WEIGHT_DECAY and shuffle- // interleaved oversampling. Removed from all four backends in sync (AI\NeuronCPU.mqh / // WarriorDML.cpp / Network.cl mirror this). w[wi] = ClampWeight(w[wi] + delta); m[wi] = mt; v[wi] = vt; } })) { ctx->lastError = 201; return 0; } return 1; } //+------------------------------------------------------------------+ //| Convolution kernels - mirror AI\Network.cl's Conv kernels 1:1. A | //| single (windowIn+1)*windowOut weight block is shared across every | //| sliding position. | //+------------------------------------------------------------------+ WARRIORCPU_API int __stdcall CPU_FeedForwardConv(CpuHandle handle, int wHandle, int iHandle, int oHandle, int inputs, int step, int windowIn, int windowOut, int activation, int positions) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *w, *in, *out; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, iHandle) || !ValidHandle(ctx, oHandle)) return 0; REQUIRE(ctx, positions >= 0 && inputs >= 0 && windowIn >= 0 && windowOut >= 0); REQUIRE(ctx, Size(ctx, iHandle) >= (size_t)inputs); REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)(windowIn + 1) * windowOut); REQUIRE(ctx, Size(ctx, oHandle) >= (size_t)positions * windowOut); w = ctx->buffers[wHandle].data.data(); in = ctx->buffers[iHandle].data.data(); out = ctx->buffers[oHandle].data.data(); } if(!ctx->pool.ParallelFor(positions, [=](int begin, int end) { for(int i = begin; i < end; i++) { int shiftOut = windowOut * i; int shiftIn = step * i; for(int o = 0; o < windowOut; o++) { int shift = (windowIn + 1) * o; int stop = (windowIn <= (inputs - shiftIn)) ? windowIn : (inputs - shiftIn); double sum = 0.0; for(int k = 0; k < stop; k++) sum += in[shiftIn + k] * w[shift + k]; sum += w[shift + windowIn]; out[o + shiftOut] = Activation(sum, activation); } } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_CalcHiddenGradientConv(CpuHandle handle, int wHandle, int gHandle, int oHandle, int igHandle, int outputs, int step, int windowIn, int windowOut, int activation, int inputCount) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *w, *g, *o, *ig; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, gHandle) || !ValidHandle(ctx, oHandle) || !ValidHandle(ctx, igHandle)) return 0; REQUIRE(ctx, inputCount >= 0 && outputs >= 0 && windowIn >= 0 && windowOut > 0 && step > 0); REQUIRE(ctx, Size(ctx, oHandle) >= (size_t)inputCount && Size(ctx, igHandle) >= (size_t)inputCount); REQUIRE(ctx, Size(ctx, gHandle) >= (size_t)outputs); REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)(windowIn + 1) * windowOut); w = ctx->buffers[wHandle].data.data(); g = ctx->buffers[gHandle].data.data(); o = ctx->buffers[oHandle].data.data(); ig = ctx->buffers[igHandle].data.data(); } if(!ctx->pool.ParallelFor(inputCount, [=](int begin, int end) { for(int i = begin; i < end; i++) { double out_v = o[i]; int start = i - windowIn + step; start = std::max((start - start % step) / step, 0); int stop = (i - i % step) / step + 1; if(stop > (outputs / windowOut)) stop = outputs / windowOut; double sum = 0.0; for(int h = 0; h < windowOut; h++) for(int k = start; k < stop; k++) { int shiftW = (stop - k - 1) * step + i % step + h * (windowIn + 1); int shiftG = k * windowOut + h; if(shiftG >= outputs || shiftW >= (windowIn + 1) * windowOut) break; sum += g[shiftG] * w[shiftW]; } if(activation == 0) { sum = std::max(-1.0, std::min(1.0, sum + out_v)) - out_v; sum *= std::max(MIN_ACTIVATION_DERIVATIVE, 1.0 - out_v * out_v); } else if(activation == 1) sum = (std::max(0.0, std::min(1.0, sum + out_v)) - out_v) * std::max(MIN_ACTIVATION_DERIVATIVE, out_v * (1.0 - out_v)); else if(activation == 2 && out_v < 0) sum *= 0.01; ig[i] = sum; } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_UpdateWeightsConvMomentum(CpuHandle handle, int wHandle, int gHandle, int iHandle, int dwHandle, int inputs, double learningRate, double momentum, int windowIn, int windowOut, int step) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; int total; size_t gSize, inSize; double *w, *g, *in, *dw; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, gHandle) || !ValidHandle(ctx, iHandle) || !ValidHandle(ctx, dwHandle)) return 0; REQUIRE(ctx, windowIn >= 0 && windowOut >= 0 && step > 0 && inputs >= 0); total = (windowIn + 1) * windowOut; REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)total && Size(ctx, dwHandle) >= (size_t)total); gSize = Size(ctx, gHandle); inSize = Size(ctx, iHandle); w = ctx->buffers[wHandle].data.data(); g = ctx->buffers[gHandle].data.data(); in = ctx->buffers[iHandle].data.data(); dw = ctx->buffers[dwHandle].data.data(); } if(!ctx->pool.ParallelFor(total, [=](int begin, int end) { for(int i = begin; i < end; i++) { int shift = i % (windowIn + 1); int shiftOut = (i - shift) / (windowIn + 1); int t = (inputs - windowIn) % step; t = (inputs - windowIn - t) / step + (t > 0 ? 1 : 0); double grad = 0.0; for(int k = 0; k < t; k++) { if(shift != windowIn && (shift + k * step) >= inputs) break; int gi = k * windowOut + shiftOut; int ii = shift + k * step; if((size_t)gi >= gSize || (shift != windowIn && (size_t)ii >= inSize)) break; grad += g[gi] * (shift == windowIn ? 1.0 : in[ii]); } double delta = learningRate * grad + momentum * dw[i]; dw[i] = delta; w[i] = ClampWeight(w[i] + delta); } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_UpdateWeightsConvAdam(CpuHandle handle, int wHandle, int gHandle, int iHandle, int mHandle, int vHandle, int inputs, double lt, double b1, double b2, int windowIn, int windowOut, int step) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; int total; size_t gSize, inSize; double *w, *g, *in, *m, *v; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, gHandle) || !ValidHandle(ctx, iHandle) || !ValidHandle(ctx, mHandle) || !ValidHandle(ctx, vHandle)) return 0; REQUIRE(ctx, windowIn >= 0 && windowOut >= 0 && step > 0 && inputs >= 0); total = (windowIn + 1) * windowOut; REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)total && Size(ctx, mHandle) >= (size_t)total && Size(ctx, vHandle) >= (size_t)total); gSize = Size(ctx, gHandle); inSize = Size(ctx, iHandle); w = ctx->buffers[wHandle].data.data(); g = ctx->buffers[gHandle].data.data(); in = ctx->buffers[iHandle].data.data(); m = ctx->buffers[mHandle].data.data(); v = ctx->buffers[vHandle].data.data(); } if(!ctx->pool.ParallelFor(windowIn + 1, [=](int begin, int end) { for(int i = begin; i < end; i++) { int t = (inputs - (windowIn - step)) % step; t = (inputs - (windowIn - step) - t) / step + (t > 0 ? 1 : 0); for(int out = 0; out < windowOut; out++) { int shiftW = i + out * (windowIn + 1); double grad = 0.0; for(int k = 0; k < t; k++) { if(i != windowIn && (i + k * step) >= inputs) break; int gi = k * windowOut + out; int ii = i + k * step; if((size_t)gi >= gSize || (i != windowIn && (size_t)ii >= inSize)) break; grad += g[gi] * (i == windowIn ? 1.0 : in[ii]); } double mt = b1 * m[shiftW] + (1.0 - b1) * grad; double vt = sqrt(b2 * v[shiftW] + (1.0 - b2) * grad * grad); double delta = ClampDelta(lt * mt / (vt > 0.0 ? vt : lt * 10.0) - lt * WEIGHT_DECAY * w[shiftW]); // Sign-agreement gate removed - see CPU_UpdateWeightsAdam's comment for why. w[shiftW] = ClampWeight(w[shiftW] + delta); m[shiftW] = mt; v[shiftW] = vt; } } })) { ctx->lastError = 201; return 0; } return 1; } //+------------------------------------------------------------------+ //| LSTM kernels - mirror AI\Network.cl's LSTM_* kernels 1:1. | //+------------------------------------------------------------------+ WARRIORCPU_API int __stdcall CPU_LSTMGates(CpuHandle handle, int wHandle, int hiddenPrevHandle, int inputsHandle, int concatenatedHandle, int hiddenSize, int inputSize) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; int perGate; double *w, *hp, *in, *cat; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, hiddenPrevHandle) || !ValidHandle(ctx, inputsHandle) || !ValidHandle(ctx, concatenatedHandle)) return 0; REQUIRE(ctx, hiddenSize >= 0 && inputSize >= 0); perGate = hiddenSize * (hiddenSize + inputSize + 1); REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)4 * perGate); REQUIRE(ctx, Size(ctx, hiddenPrevHandle) >= (size_t)hiddenSize && Size(ctx, inputsHandle) >= (size_t)inputSize); REQUIRE(ctx, Size(ctx, concatenatedHandle) >= (size_t)4 * hiddenSize); w = ctx->buffers[wHandle].data.data(); hp = ctx->buffers[hiddenPrevHandle].data.data(); in = ctx->buffers[inputsHandle].data.data(); cat = ctx->buffers[concatenatedHandle].data.data(); } if(!ctx->pool.ParallelFor(hiddenSize, [=](int begin, int end) { for(int hid = begin; hid < end; hid++) for(int gate = 0; gate < 4; gate++) { int shift = gate * perGate + hid * (hiddenSize + inputSize + 1); double sum = 0.0; for(int k = 0; k < hiddenSize; k++) sum += hp[k] * w[shift + k]; for(int k = 0; k < inputSize; k++) sum += in[k] * w[shift + hiddenSize + k]; sum += w[shift + hiddenSize + inputSize]; double val = (gate < 3) ? (1.0 / (1.0 + exp(-sum))) : tanh(sum); cat[gate * hiddenSize + hid] = val; } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_LSTMState(CpuHandle handle, int concatenatedHandle, int memoryHandle, int hiddenPrevHandle, int hiddenCacheHandle, int outputHandle, int hiddenSize) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *cat, *mem, *hp, *hc, *out; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, concatenatedHandle) || !ValidHandle(ctx, memoryHandle) || !ValidHandle(ctx, hiddenPrevHandle) || !ValidHandle(ctx, hiddenCacheHandle) || !ValidHandle(ctx, outputHandle)) return 0; REQUIRE(ctx, hiddenSize >= 0); REQUIRE(ctx, Size(ctx, concatenatedHandle) >= (size_t)4 * hiddenSize); REQUIRE(ctx, Size(ctx, memoryHandle) >= (size_t)2 * hiddenSize); REQUIRE(ctx, Size(ctx, hiddenPrevHandle) >= (size_t)hiddenSize && Size(ctx, hiddenCacheHandle) >= (size_t)hiddenSize && Size(ctx, outputHandle) >= (size_t)hiddenSize); cat = ctx->buffers[concatenatedHandle].data.data(); mem = ctx->buffers[memoryHandle].data.data(); hp = ctx->buffers[hiddenPrevHandle].data.data(); hc = ctx->buffers[hiddenCacheHandle].data.data(); out = ctx->buffers[outputHandle].data.data(); } if(!ctx->pool.ParallelFor(hiddenSize, [=](int begin, int end) { for(int i = begin; i < end; i++) { double f = cat[i]; double ii = cat[hiddenSize + i]; double o = cat[2 * hiddenSize + i]; double gg = cat[3 * hiddenSize + i]; double c_prev = mem[i]; mem[hiddenSize + i] = c_prev; double c_t = f * c_prev + ii * gg; mem[i] = c_t; hc[i] = hp[i]; out[i] = o * tanh(c_t); } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_LSTMGateGradient(CpuHandle handle, int gradientHandle, int memoryHandle, int concatenatedHandle, int concatenatedGradientHandle, int hiddenSize) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *grad, *mem, *cat, *cg; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, gradientHandle) || !ValidHandle(ctx, memoryHandle) || !ValidHandle(ctx, concatenatedHandle) || !ValidHandle(ctx, concatenatedGradientHandle)) return 0; REQUIRE(ctx, hiddenSize >= 0); REQUIRE(ctx, Size(ctx, gradientHandle) >= (size_t)hiddenSize); REQUIRE(ctx, Size(ctx, memoryHandle) >= (size_t)2 * hiddenSize); REQUIRE(ctx, Size(ctx, concatenatedHandle) >= (size_t)4 * hiddenSize && Size(ctx, concatenatedGradientHandle) >= (size_t)4 * hiddenSize); grad = ctx->buffers[gradientHandle].data.data(); mem = ctx->buffers[memoryHandle].data.data(); cat = ctx->buffers[concatenatedHandle].data.data(); cg = ctx->buffers[concatenatedGradientHandle].data.data(); } if(!ctx->pool.ParallelFor(hiddenSize, [=](int begin, int end) { for(int i = begin; i < end; i++) { double c_t = mem[i]; double c_prev = mem[hiddenSize + i]; double f = cat[i]; double ii = cat[hiddenSize + i]; double o = cat[2 * hiddenSize + i]; double g = cat[3 * hiddenSize + i]; double t = tanh(c_t); double dh = grad[i]; // Derivative floor on every gate below - see MIN_ACTIVATION_DERIVATIVE's declaration // comment. LSTM gates are sigmoid/tanh by design and routinely sit near their saturated // extremes (a fully "open" or "closed" gate), which is exactly where an unfloored // derivative goes to zero and blocks gradient from flowing through the recurrent path at // all. double dc = dh * o * std::max(MIN_ACTIVATION_DERIVATIVE, 1.0 - t * t); cg[2 * hiddenSize + i] = dh * t * std::max(MIN_ACTIVATION_DERIVATIVE, o * (1.0 - o)); cg[i] = dc * c_prev * std::max(MIN_ACTIVATION_DERIVATIVE, f * (1.0 - f)); cg[hiddenSize + i] = dc * g * std::max(MIN_ACTIVATION_DERIVATIVE, ii * (1.0 - ii)); cg[3 * hiddenSize + i] = dc * ii * std::max(MIN_ACTIVATION_DERIVATIVE, 1.0 - g * g); } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_LSTMWeightsGradient(CpuHandle handle, int concatenatedGradientHandle, int hiddenCacheHandle, int inputsHandle, int weightsGradientHandle, int hiddenSize, int inputSize) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; int perGate, total; double *cg, *hc, *in, *wg; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, concatenatedGradientHandle) || !ValidHandle(ctx, hiddenCacheHandle) || !ValidHandle(ctx, inputsHandle) || !ValidHandle(ctx, weightsGradientHandle)) return 0; REQUIRE(ctx, hiddenSize >= 0 && inputSize >= 0); perGate = hiddenSize * (hiddenSize + inputSize + 1); total = 4 * perGate; REQUIRE(ctx, Size(ctx, concatenatedGradientHandle) >= (size_t)4 * hiddenSize); REQUIRE(ctx, Size(ctx, hiddenCacheHandle) >= (size_t)hiddenSize && Size(ctx, inputsHandle) >= (size_t)inputSize); REQUIRE(ctx, Size(ctx, weightsGradientHandle) >= (size_t)total); cg = ctx->buffers[concatenatedGradientHandle].data.data(); hc = ctx->buffers[hiddenCacheHandle].data.data(); in = ctx->buffers[inputsHandle].data.data(); wg = ctx->buffers[weightsGradientHandle].data.data(); } if(!ctx->pool.ParallelFor(total, [=](int begin, int end) { for(int wi = begin; wi < end; wi++) { int gate = wi / perGate; int rem = wi % perGate; int hid = rem / (hiddenSize + inputSize + 1); int k = rem % (hiddenSize + inputSize + 1); double inp = (k < hiddenSize) ? hc[k] : ((k < hiddenSize + inputSize) ? in[k - hiddenSize] : 1.0); wg[wi] = cg[gate * hiddenSize + hid] * inp; } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_LSTMInputsGradient(CpuHandle handle, int concatenatedGradientHandle, int wHandle, int inputsGradientHandle, int hiddenSize, int inputSize) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; int perGate; double *cg, *w, *ig; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, concatenatedGradientHandle) || !ValidHandle(ctx, wHandle) || !ValidHandle(ctx, inputsGradientHandle)) return 0; REQUIRE(ctx, hiddenSize >= 0 && inputSize >= 0); perGate = hiddenSize * (hiddenSize + inputSize + 1); REQUIRE(ctx, Size(ctx, concatenatedGradientHandle) >= (size_t)4 * hiddenSize); REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)4 * perGate); REQUIRE(ctx, Size(ctx, inputsGradientHandle) >= (size_t)inputSize); cg = ctx->buffers[concatenatedGradientHandle].data.data(); w = ctx->buffers[wHandle].data.data(); ig = ctx->buffers[inputsGradientHandle].data.data(); } if(!ctx->pool.ParallelFor(inputSize, [=](int begin, int end) { for(int j = begin; j < end; j++) { double sum = 0.0; for(int gate = 0; gate < 4; gate++) for(int hid = 0; hid < hiddenSize; hid++) sum += cg[gate * hiddenSize + hid] * w[gate * perGate + hid * (hiddenSize + inputSize + 1) + hiddenSize + j]; ig[j] = sum; } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_LSTMUpdateWeightsAdam(CpuHandle handle, int wHandle, int weightsGradientHandle, int mHandle, int vHandle, double l, double b1, double b2, int total) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *w, *wg, *m, *v; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, weightsGradientHandle) || !ValidHandle(ctx, mHandle) || !ValidHandle(ctx, vHandle)) return 0; REQUIRE(ctx, total >= 0); REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)total && Size(ctx, weightsGradientHandle) >= (size_t)total); REQUIRE(ctx, Size(ctx, mHandle) >= (size_t)total && Size(ctx, vHandle) >= (size_t)total); w = ctx->buffers[wHandle].data.data(); wg = ctx->buffers[weightsGradientHandle].data.data(); m = ctx->buffers[mHandle].data.data(); v = ctx->buffers[vHandle].data.data(); } if(!ctx->pool.ParallelFor(total, [=](int begin, int end) { for(int wi = begin; wi < end; wi++) { double g = wg[wi]; double mt = b1 * m[wi] + (1.0 - b1) * g; double vt = sqrt(b2 * v[wi] + (1.0 - b2) * g * g); double delta = ClampDelta(l * mt / (vt > 0.0 ? vt : l * 10.0) - l * WEIGHT_DECAY * w[wi]); // Sign-agreement gate removed - see CPU_UpdateWeightsAdam's comment for why. w[wi] = ClampWeight(w[wi] + delta); m[wi] = mt; v[wi] = vt; } })) { ctx->lastError = 201; return 0; } return 1; } // SGD+momentum counterpart to CPU_LSTMUpdateWeightsAdam - same flat // (already-elementwise) weights_gradient/DeltaWeightsLSTM layout, just the // classic heavy-ball update instead of Adam's per-parameter adaptive step. // See CPU_UpdateWeightsMomentum's matching comment for why the dense-layer // version doesn't need this treatment: this is the LSTM path's counterpart, // so it always uses the flat total-weight indexing already established by // CPU_LSTMUpdateWeightsAdam above. WARRIORCPU_API int __stdcall CPU_LSTMUpdateWeightsMomentum(CpuHandle handle, int wHandle, int weightsGradientHandle, int dwHandle, double learningRate, double momentum, int total) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *w, *wg, *dw; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, wHandle) || !ValidHandle(ctx, weightsGradientHandle) || !ValidHandle(ctx, dwHandle)) return 0; REQUIRE(ctx, total >= 0); REQUIRE(ctx, Size(ctx, wHandle) >= (size_t)total && Size(ctx, weightsGradientHandle) >= (size_t)total); REQUIRE(ctx, Size(ctx, dwHandle) >= (size_t)total); w = ctx->buffers[wHandle].data.data(); wg = ctx->buffers[weightsGradientHandle].data.data(); dw = ctx->buffers[dwHandle].data.data(); } if(!ctx->pool.ParallelFor(total, [=](int begin, int end) { for(int wi = begin; wi < end; wi++) { double delta = learningRate * wg[wi] + momentum * dw[wi]; dw[wi] = delta; w[wi] = ClampWeight(w[wi] + delta); } })) { ctx->lastError = 201; return 0; } return 1; } //+------------------------------------------------------------------+ //| Max-pooling layer - no weights, mirrors AI\Network.cl's | //| FeedForwardProof/CalcInputGradientProof. | //+------------------------------------------------------------------+ WARRIORCPU_API int __stdcall CPU_FeedForwardProof(CpuHandle handle, int iHandle, int oHandle, int inputs, int window, int step, int outputs) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; size_t inSize; double *in, *out; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, iHandle) || !ValidHandle(ctx, oHandle)) return 0; REQUIRE(ctx, outputs >= 0 && inputs >= 0 && step > 0); REQUIRE(ctx, Size(ctx, iHandle) >= (size_t)inputs && Size(ctx, oHandle) >= (size_t)outputs); inSize = Size(ctx, iHandle); in = ctx->buffers[iHandle].data.data(); out = ctx->buffers[oHandle].data.data(); } if(!ctx->pool.ParallelFor(outputs, [=](int begin, int end) { for(int i = begin; i < end; i++) { int pos = i * step; if((size_t)pos >= inSize) { out[i] = 0.0; continue; } double result = in[pos]; for(int k = 1; k < window; k++) { int shift = k + pos; if(shift >= inputs || (size_t)shift >= inSize) break; result = std::max(result, in[shift]); } out[i] = result; } })) { ctx->lastError = 201; return 0; } return 1; } WARRIORCPU_API int __stdcall CPU_CalcInputGradientProof(CpuHandle handle, int iHandle, int gHandle, int oHandle, int igHandle, int outputs, int window, int step, int inputs) { CpuContext *ctx = reinterpret_cast(handle); if(!ctx) return 0; double *in, *g, *o, *ig; { // see CpuContext's declaration comment - lock scope stops here, before the blocking ParallelFor std::lock_guard lock(ctx->mutex); if(!ValidHandle(ctx, iHandle) || !ValidHandle(ctx, gHandle) || !ValidHandle(ctx, oHandle) || !ValidHandle(ctx, igHandle)) return 0; REQUIRE(ctx, inputs >= 0 && outputs >= 0 && step > 0); REQUIRE(ctx, Size(ctx, iHandle) >= (size_t)inputs && Size(ctx, igHandle) >= (size_t)inputs); REQUIRE(ctx, Size(ctx, oHandle) >= (size_t)outputs && Size(ctx, gHandle) >= (size_t)outputs); in = ctx->buffers[iHandle].data.data(); g = ctx->buffers[gHandle].data.data(); o = ctx->buffers[oHandle].data.data(); ig = ctx->buffers[igHandle].data.data(); } if(!ctx->pool.ParallelFor(inputs, [=](int begin, int end) { for(int i = begin; i < end; i++) { double value = in[i]; int start = i - window + step; start = (start - start % step) / step; int stop = (i - i % step) / step + 1; double prevGrad = 0.0; for(int out = std::max(0, start); out < std::min(outputs, stop); out++) if(value == o[out]) prevGrad += g[out]; ig[i] = prevGrad; } })) { ctx->lastError = 201; return 0; } return 1; }