Warrior_EA/AI/BufferDouble.mqh
AnimateDread 274630f802 fix: training-stability audit fixes F1/F2/F3/F5 - unbiased shuffle, real plateau escapes, fresh optimizer state on restore, pure OOS metric
Four of the six findings from research/training_pipeline_audit_2026-08-09.md
(F4 mini-batching and F6 feature re-encode deliberately deferred - see the
report's implementation-status section for why):

- F1: pass-2 Fisher-Yates (and AutoTune's MI block shuffle) used MathRand()%,
  which is 15-bit - provably non-uniform on every full-history era over 32,768
  queued samples. New 30-bit ShuffleRandomIndex().
- F2: plateau warm restarts were a no-op whenever eta already sat at its
  ceiling (the normal state of a non-regressing plateau) - the ladder was just
  a 24-era countdown. Restarts now overshoot to 5x the ceiling
  (PLATEAU_RESTART_BOOST) and anneal geometrically back over the patience
  window, SGDR-style; ETA_MIN widened 1e-4 -> 1e-5 so the decay schedule has
  real range.
- F3: checkpoint restores put weights back but kept the rejected trajectory's
  Adam moments, so the optimizer immediately pushed back toward the rolled-back
  state (the restore->regress->restore oscillation). CNet::ResetOptimizerState()
  zeroes moments/momentum/step counters (weights, BN statistics, gamma/beta
  untouched) on every mid-run restore, every boosted restart, and the
  deploy-time restore that online learning continues from.
- F5: batch-norm running statistics now freeze for the pass-3 OOS scoring walk,
  so the selection metric the checkpoint ranking and deploy gate read is a pure
  function of the checkpoint instead of partly measuring BN drift. Defensive
  unfreeze in FinalizeTrainRun covers stop-mid-pass; live/online adaptation and
  the OOS continual-learning simulation stay adaptive by design.

Compiled clean (0 errors, 0 warnings) via the staged-tree recipe.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 10:54:09 -04:00

242 lines
10 KiB
MQL5

//+------------------------------------------------------------------+
//| BufferDouble.mqh |
//| AnimateDread |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
//| CBufferDouble - the OpenCL/DirectML-backed buffer wrapper every |
//| *OCL neuron class (AI\Network.mqh) uses for its weights/output/ |
//| gradient storage. Needs COpenCLMy (AI\Network.mqh) and |
//| CDirectMLMy (AI\NeuronDirectML.mqh) already declared. Extracted |
//| verbatim out of AI\Network.mqh (SOLID cleanup) - no logic changes.|
//+------------------------------------------------------------------+
#include "NeuronDirectML.mqh"
class CBufferDouble : public CArrayDouble
{
protected:
COpenCLMy *OpenCL;
CDirectMLMy *DirectML;
int m_myIndex;
//--- OpenCL device buffers are float32 (see AI\Network.cl's fp32 conversion) while this class's
//--- public CArrayDouble interface - and the DirectML backend's DLL boundary (DML_BufferRead/
//--- DML_BufferWrite, hardcoded to double[]) - stay double. This scratch array is the narrow/widen
//--- point: populated from m_data before an OpenCL write, copied back into m_data after an OpenCL
//--- read. Never touched on the DirectML or CPU-fallback paths.
float m_data_f[];
public:
CBufferDouble(void);
~CBufferDouble(void);
//---
virtual bool BufferInit(uint count, double value);
virtual bool BufferCreate(COpenCLMy *opencl);
virtual bool BufferCreate(CDirectMLMy *directml);
virtual bool BufferFree(void);
virtual bool BufferRead(void);
virtual bool BufferWrite(void);
virtual int GetData(double &values[]);
virtual int GetData(CArrayDouble *values);
virtual int GetIndex(void) { return m_myIndex; }
//---
virtual int Type(void) const { return defBufferDouble; }
};
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CBufferDouble::CBufferDouble(void) : m_myIndex(-1)
{
OpenCL = NULL;
DirectML = NULL;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
CBufferDouble::~CBufferDouble(void)
{
BufferFree();
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CBufferDouble::BufferCreate(COpenCLMy *opencl)
{
BufferFree();
//---
if(CheckPointer(opencl) == POINTER_INVALID)
return false;
if(ArrayResize(m_data_f, m_data_total) < 0)
{
Print(__FUNCTION__ + ": ArrayResize(m_data_f, " + IntegerToString(m_data_total) + ") failed - allocation failure?");
return false;
}
for(int i = 0; i < m_data_total; i++)
m_data_f[i] = (float)m_data[i];
if((m_myIndex = opencl.AddBufferFromArray(m_data_f, 0, m_data_total, CL_MEM_READ_WRITE | CL_MEM_ALLOC_HOST_PTR)) < 0)
{
Print(__FUNCTION__ + ": OpenCL AddBufferFromArray failed for " + IntegerToString(m_data_total) + " elements, error " + IntegerToString(GetLastError()) + " (VRAM exhaustion / device lost?)");
return false;
}
OpenCL = opencl;
//---
return true;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CBufferDouble::BufferCreate(CDirectMLMy *directml)
{
BufferFree();
//---
if(CheckPointer(directml) == POINTER_INVALID)
return false;
if((m_myIndex = directml.BufferCreate(m_data_total)) < 0)
{
Print(__FUNCTION__ + ": " + directml.BackendName() + " BufferCreate failed for " + IntegerToString(m_data_total) + " elements (error " + IntegerToString(directml.LastError()) + ")");
return false;
}
DirectML = directml;
bool ok = DirectML.BufferWrite(m_myIndex, m_data, m_data_total);
if(!ok)
Print(__FUNCTION__ + ": " + directml.BackendName() + " BufferWrite failed for buffer " + IntegerToString(m_myIndex) +
" on create (" + IntegerToString(m_data_total) + " elements, error " + IntegerToString(directml.LastError()) + ")");
return ok;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CBufferDouble::BufferFree(void)
{
if(CheckPointer(OpenCL) != POINTER_INVALID && m_myIndex >= 0)
{
if(!OpenCL.BufferFree(m_myIndex))
return false;
m_myIndex = -1;
OpenCL = NULL;
return true;
}
if(CheckPointer(DirectML) != POINTER_INVALID && m_myIndex >= 0)
{
DirectML.BufferFree(m_myIndex);
m_myIndex = -1;
DirectML = NULL;
return true;
}
//---
return false;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CBufferDouble::BufferRead(void)
{
if(CheckPointer(OpenCL) != POINTER_INVALID && m_myIndex >= 0)
{
if(ArrayResize(m_data_f, m_data_total) < 0)
{
Print(__FUNCTION__ + ": ArrayResize(m_data_f, " + IntegerToString(m_data_total) + ") failed - allocation failure?");
return false;
}
if(!OpenCL.BufferRead(m_myIndex, m_data_f, 0, 0, m_data_total))
{
Print(__FUNCTION__ + ": OpenCL BufferRead failed for buffer " + IntegerToString(m_myIndex) + ", error " + IntegerToString(GetLastError()));
return false;
}
for(int i = 0; i < m_data_total; i++)
m_data[i] = (double)m_data_f[i];
return true;
}
if(CheckPointer(DirectML) != POINTER_INVALID && m_myIndex >= 0)
{
bool ok = DirectML.BufferRead(m_myIndex, m_data, m_data_total);
if(!ok)
Print(__FUNCTION__ + ": " + DirectML.BackendName() + " BufferRead failed for buffer " + IntegerToString(m_myIndex) +
" (" + IntegerToString(m_data_total) + " elements, error " + IntegerToString(DirectML.LastError()) + ")");
return ok;
}
//---
return false;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CBufferDouble::BufferWrite(void)
{
if(CheckPointer(OpenCL) != POINTER_INVALID && m_myIndex >= 0)
{
if(ArrayResize(m_data_f, m_data_total) < 0)
{
Print(__FUNCTION__ + ": ArrayResize(m_data_f, " + IntegerToString(m_data_total) + ") failed - allocation failure?");
return false;
}
for(int i = 0; i < m_data_total; i++)
m_data_f[i] = (float)m_data[i];
bool ok = OpenCL.BufferWrite(m_myIndex, m_data_f, 0, 0, m_data_total);
if(!ok)
Print(__FUNCTION__ + ": OpenCL BufferWrite failed for buffer " + IntegerToString(m_myIndex) + ", error " + IntegerToString(GetLastError()));
return ok;
}
if(CheckPointer(DirectML) != POINTER_INVALID && m_myIndex >= 0)
{
bool ok = DirectML.BufferWrite(m_myIndex, m_data, m_data_total);
if(!ok)
Print(__FUNCTION__ + ": " + DirectML.BackendName() + " BufferWrite failed for buffer " + IntegerToString(m_myIndex) +
" (" + IntegerToString(m_data_total) + " elements, error " + IntegerToString(DirectML.LastError()) + ")");
return ok;
}
//---
return false;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
bool CBufferDouble::BufferInit(uint count, double value)
{
if(!Reserve(count))
return false;
m_data_total = (int)fmin(ArrayInitialize(m_data, value), count);
//---
return m_data_total == count;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
int CBufferDouble::GetData(double &values[])
{
if(!BufferRead())
return false;
return ArrayCopy(values, m_data, 0, 0, m_data_total);
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
int CBufferDouble::GetData(CArrayDouble *values)
{
if(!BufferRead())
return -1;
values.Clear();
if(!values.AddArray(GetPointer(this)))
return -1;
return m_data_total;
}
//+------------------------------------------------------------------+
//| Zero an optimizer-state buffer (Adam moments / SGD momentum) in |
//| place, host AND device copies. Part of the 2026-08-09 audit's F3 |
//| fix: CNet::RestoreWeights() puts the WEIGHTS back but the moment |
//| buffers still hold the rejected trajectory's momentum, so the very|
//| next updates push the restored weights straight back toward the |
//| state that was just rolled back. See CNet::ResetOptimizerState. |
//| A missing/empty buffer is success - not every neuron owns moments |
//| (output-layer neurons, pool layers, SGD nets have no m/v). |
//+------------------------------------------------------------------+
bool ZeroOptimizerBuffer(CBufferDouble *buf)
{
if(CheckPointer(buf) == POINTER_INVALID || buf.Total() <= 0)
return true;
int total = buf.Total();
for(int i = 0; i < total; i++)
if(!buf.Update(i, 0.0))
return false;
//--- push to the device-side copy only when one exists; a host-only buffer (pure-MQL5 inference
//--- mode, which never trains) has no index and BufferWrite would report a spurious failure.
if(buf.GetIndex() >= 0)
return buf.BufferWrite();
return true;
}