our_knp/readout/build_delta_cpu.py
int 151e9f0b23 Inference-on-KNP pipeline: altai_infer, offline model building and readout training
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 10:42:35 +03:00

111 lines
5.9 KiB
Python

"""Build an isolated CPU single-threaded backend with an error-driven (delta-rule) dopamine plasticity option.
KNP_DELTA_RULE=<rate> (environment, read once): for every AltAI STDP neuron with non-zero dopamine D on a step,
each trainable synapse whose presynaptic spike arrived KNP_DELTA_LAG steps earlier changes its synaptic resource by
rate * D, whether or not the neuron itself spiked; stability is not used. KNP_DELTA_DECAY=<d> additionally changes
every other synapse of that neuron by -rate * D * d (competitive learning). Without the variable the backend behaves like
the stock one. Original sources and libraries are not modified.
usage: python3 build_delta_cpu.py → /home/int/run/delta-cpu/libknp-cpu-delta-backend.so
"""
from pathlib import Path
import shlex
import shutil
import subprocess
root = Path('/home/int/run/delta-cpu')
root.mkdir(parents=True, exist_ok=True)
source_root = Path('/mnt/c/KNP/examples/knp/backends/cpu')
shutil.copytree(source_root / 'cpu-library/include', root / 'include', dirs_exist_ok=True)
altai = root / 'include/knp/backends/cpu-library/impl/populations/altai/altai_stdp.h'
s = altai.read_text()
marker = ''' for (size_t neuron_index = 0; neuron_index < population.size(); ++neuron_index)
{
auto &neuron = population[neuron_index];
// Dopamine processing. Dopamine punishment if forced does nothing.'''
assert marker in s
s = s.replace(marker, ''' // Experimental delta rule (KNP_DELTA_RULE=<rate>), see build_delta_cpu.py.
// Step offset between the dopamine step and the presynaptic arrival step (KNP_DELTA_LAG, default 0).
static const int64_t DELTA_LAG = []
{
const char *value = std::getenv("KNP_DELTA_LAG");
return value ? std::stol(value) : 0L;
}();
// Competitive (winner-take-all) learning: with KNP_DELTA_DECAY=<d>, synapses whose input did not arrive change by
// -rate * D * d, so a rewarded neuron moves its weights towards the current input pattern.
static const float delta_decay = []
{
const char *value = std::getenv("KNP_DELTA_DECAY");
return value ? std::stof(value) : 0.F;
}();
// Competitive rule of k-means type: with KNP_DELTA_WDECAY=<c>, every synapse of the neuron also changes by
// -rate * D * c * weight, so with dopamine after the neuron's own spikes the weight settles where
// c * weight equals the probability that its input was active (dw ~ input - c * w).
static const float delta_wdecay = []
{
const char *value = std::getenv("KNP_DELTA_WDECAY");
return value ? std::stof(value) : 0.F;
}();
static const float delta_rate = []
{
const char *value = std::getenv("KNP_DELTA_RULE");
return value ? std::stof(value) : 0.F;
}();
for (size_t neuron_index = 0; neuron_index < population.size(); ++neuron_index)
{
auto &neuron = population[neuron_index];
if (delta_rate > 0)
{
if (neuron.dopamine_value_ == 0.0) continue;
std::vector<std::reference_wrapper<SynapseParamType>> delta_synapses =
training::stdp::get_all_connected_synapses<SynapseType>(working_projections, neuron_index);
static const bool debug = std::getenv("KNP_DELTA_DEBUG") != nullptr;
static uint64_t calls = 0, hist[7] = {0, 0, 0, 0, 0, 0, 0};
for (auto &synapse : delta_synapses)
{
const int64_t lag = static_cast<int64_t>(step) -
static_cast<int64_t>(synapse.get().rule_.last_spike_step_ + synapse.get().delay_ - 1);
if (debug && lag >= -3 && lag <= 3) ++hist[lag + 3];
// The presynaptic spike reaches the neuron on this step.
if (lag == DELTA_LAG)
synapse.get().rule_.synaptic_resource_ += delta_rate * neuron.dopamine_value_;
else if (delta_decay > 0)
synapse.get().rule_.synaptic_resource_ -= delta_rate * neuron.dopamine_value_ * delta_decay;
if (delta_wdecay > 0)
synapse.get().rule_.synaptic_resource_ -=
delta_rate * neuron.dopamine_value_ * delta_wdecay * synapse.get().weight_;
}
if (debug && ++calls % 500 == 0)
std::fprintf(stderr, "KNP_DELTA_DEBUG calls=%lu lag-3..3: %lu %lu %lu %lu %lu %lu %lu\\n",
static_cast<unsigned long>(calls), hist[0], hist[1], hist[2], hist[3], hist[4], hist[5], hist[6]);
training::stdp::recalculate_synapse_weights(delta_synapses);
continue;
}
// Dopamine processing. Dopamine punishment if forced does nothing.''')
if '#include <cstdlib>' not in s:
s = s.replace('#include <algorithm>', '#include <algorithm>\n#include <cstdio>\n#include <cstdlib>\n#include <string>', 1)
altai.write_text(s)
build = Path('/home/int/knp-build/knp/backends/cpu/cpu-single-threaded-backend')
folder = build / 'CMakeFiles/knp-cpu-single-threaded-backend.dir'
flags = {}
for line in (folder / 'flags.make').read_text().splitlines():
if ' = ' in line:
key, value = line.split(' = ', 1)
flags[key] = shlex.split(value)
obj = root / 'backend.o'
subprocess.run(['/usr/bin/c++', *flags['CXX_DEFINES'], '-I' + str(root / 'include'),
*flags['CXX_INCLUDES'], *flags['CXX_FLAGS'], '-c',
str(source_root / 'cpu-single-threaded-backend/impl/backend.cpp'), '-o', str(obj)], check=True)
link = shlex.split((folder / 'link.txt').read_text())
rebuilt = []
for item in link:
if item.startswith('-Wl,--dependency-file='):
continue
if item.endswith('/impl/backend.cpp.o'):
item = str(obj)
elif item.startswith('-Wl,-soname,'):
item = '-Wl,-soname,libknp-cpu-delta-backend.so'
rebuilt.append(item)
rebuilt[rebuilt.index('-o') + 1] = str(root / 'libknp-cpu-delta-backend.so')
subprocess.run(rebuilt, cwd=build, check=True)
print(root / 'libknp-cpu-delta-backend.so', flush=True)