Performance improvements, better readme and complete python bindings
🧪 Test / test (push) Failing after 28m30s
🧪 Test / test (push) Failing after 28m30s
This commit is contained in:
@@ -26,6 +26,9 @@ namespace nb = nanobind;
|
||||
// them via IVariantChannel adapters. The variant only lives at the boundary;
|
||||
// each node's internal Channel<T> stores raw T values.
|
||||
|
||||
template<typename Variant>
|
||||
class PyNode; // forward declaration
|
||||
|
||||
template<typename Variant>
|
||||
class PyNetwork {
|
||||
public:
|
||||
@@ -43,8 +46,6 @@ public:
|
||||
}
|
||||
|
||||
// connect(src_name, out_idx, dst_name, in_idx)
|
||||
// Wires src's output port out_idx to dst's input port in_idx.
|
||||
// Type check: both sides must carry the same T.
|
||||
void connect(const std::string& src_name, std::size_t out_idx,
|
||||
const std::string& dst_name, std::size_t in_idx)
|
||||
{
|
||||
@@ -65,7 +66,6 @@ public:
|
||||
dst_name + ".input[" + std::to_string(in_idx) +
|
||||
"] (" + dst.input_type(in_idx).name() + ")");
|
||||
|
||||
// The destination node owns the input channel — get it, then tell src to use it.
|
||||
auto ch = dst.input_channel(in_idx);
|
||||
src.set_output_channel(out_idx, std::move(ch));
|
||||
adj_[src_name].push_back(dst_name);
|
||||
@@ -92,16 +92,12 @@ public:
|
||||
|
||||
// ── Python tap/inject ─────────────────────────────────────────────────────
|
||||
|
||||
// Read one value from node's output port. Releases GIL while blocking.
|
||||
nb::object read(const std::string& node_name, std::size_t out_idx) {
|
||||
// We need a channel that sits on the output of this node.
|
||||
// read() installs a tap channel if not already present.
|
||||
auto key = tap_key(node_name, out_idx);
|
||||
if (!taps_.count(key)) {
|
||||
auto& src = node_at(node_name);
|
||||
if (out_idx >= src.output_count())
|
||||
throw std::out_of_range(node_name + ": output index out of range");
|
||||
// Create a tap channel matching the output type and wire it
|
||||
auto tap = make_tap_channel(src.output_type(out_idx));
|
||||
src.set_output_channel(out_idx, tap);
|
||||
taps_[key] = std::move(tap);
|
||||
@@ -114,7 +110,6 @@ public:
|
||||
return variant_to_python(std::move(v));
|
||||
}
|
||||
|
||||
// Write a Python value into node's input port. Releases GIL while blocking.
|
||||
void write(const std::string& node_name, std::size_t in_idx, nb::object value) {
|
||||
auto& dst = node_at(node_name);
|
||||
if (in_idx >= dst.input_count())
|
||||
@@ -128,8 +123,31 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
// ── Converter registration ────────────────────────────────────────────────
|
||||
// Called once per type at module init time to register to/from Python converters.
|
||||
// ── Python-callable node creation ─────────────────────────────────────────
|
||||
// Creates a PyNode wrapping a Python callable and adds it to the graph.
|
||||
// Type names must have been registered via register_full_type<T>().
|
||||
|
||||
void add_node_python(std::string name, nb::object callable,
|
||||
std::vector<std::string> in_names,
|
||||
std::vector<std::string> out_names,
|
||||
std::size_t capacity = 5)
|
||||
{
|
||||
std::vector<std::type_index> in_types, out_types;
|
||||
for (auto& s : in_names) in_types.push_back(resolve_type_name(s));
|
||||
for (auto& s : out_names) out_types.push_back(resolve_type_name(s));
|
||||
|
||||
add(std::move(name),
|
||||
std::make_shared<PyNode<Variant>>(
|
||||
std::move(callable),
|
||||
std::move(in_types),
|
||||
std::move(out_types),
|
||||
to_python_,
|
||||
from_python_,
|
||||
ch_factories_,
|
||||
capacity));
|
||||
}
|
||||
|
||||
// ── Type converter registration ───────────────────────────────────────────
|
||||
|
||||
template<typename T>
|
||||
void register_type(
|
||||
@@ -143,6 +161,52 @@ public:
|
||||
};
|
||||
}
|
||||
|
||||
// register_channel_factory<T>: registers factory for creating input channels.
|
||||
template<typename T>
|
||||
void register_channel_factory() {
|
||||
ch_factories_[std::type_index(typeid(T))] =
|
||||
[](std::size_t cap) -> std::shared_ptr<VChannel> {
|
||||
return std::make_shared<VariantChannel<T, Variant>>(
|
||||
std::make_shared<Channel<T>>(cap));
|
||||
};
|
||||
}
|
||||
|
||||
// Backward-compatible alias.
|
||||
template<typename T>
|
||||
void register_tap_factory(std::size_t = 5) {
|
||||
register_channel_factory<T>();
|
||||
}
|
||||
|
||||
// register_full_type<T>: registers converters + channel factory + type name.
|
||||
// This is what auto_bind.hpp calls; manual bindings can call register_type +
|
||||
// register_tap_factory separately for backward compatibility.
|
||||
template<typename T>
|
||||
void register_full_type(
|
||||
std::function<nb::object(const T&)> to_py,
|
||||
std::function<T(nb::object)> from_py,
|
||||
const char* friendly_name = nullptr)
|
||||
{
|
||||
register_type<T>(std::move(to_py), std::move(from_py));
|
||||
register_channel_factory<T>();
|
||||
auto idx = std::type_index(typeid(T));
|
||||
type_names_.insert_or_assign(typeid(T).name(), idx);
|
||||
if (friendly_name) type_names_.insert_or_assign(friendly_name, idx);
|
||||
}
|
||||
|
||||
// ── Type name lookup ──────────────────────────────────────────────────────
|
||||
|
||||
void register_type_name(const std::string& name, std::type_index idx) {
|
||||
type_names_.insert_or_assign(name, idx);
|
||||
}
|
||||
|
||||
std::type_index resolve_type_name(const std::string& name) const {
|
||||
auto it = type_names_.find(name);
|
||||
if (it == type_names_.end())
|
||||
throw std::runtime_error(
|
||||
"type '" + name + "' not registered — call register_full_type<T>() first");
|
||||
return it->second;
|
||||
}
|
||||
|
||||
private:
|
||||
VNode& node_at(const std::string& name) {
|
||||
auto it = nodes_.find(name);
|
||||
@@ -166,15 +230,14 @@ private:
|
||||
return node + ":" + std::to_string(idx);
|
||||
}
|
||||
|
||||
std::shared_ptr<VChannel> make_tap_channel(std::type_index type) {
|
||||
// Create the right VariantChannel<T> based on the registered type index.
|
||||
// We need a factory registered per type — stored in tap_factories_.
|
||||
auto it = tap_factories_.find(type);
|
||||
if (it == tap_factories_.end())
|
||||
std::shared_ptr<VChannel> make_tap_channel(std::type_index type,
|
||||
std::size_t cap = 5) {
|
||||
auto it = ch_factories_.find(type);
|
||||
if (it == ch_factories_.end())
|
||||
throw std::runtime_error(
|
||||
"no tap factory for type: " + std::string(type.name()) +
|
||||
" — was register_type() called for this type?");
|
||||
return it->second();
|
||||
"no channel factory for type: " + std::string(type.name()) +
|
||||
" — call register_full_type<T>() or register_tap_factory<T>()");
|
||||
return it->second(cap);
|
||||
}
|
||||
|
||||
nb::object variant_to_python(Variant v) {
|
||||
@@ -194,18 +257,6 @@ private:
|
||||
return it->second(std::move(obj));
|
||||
}
|
||||
|
||||
public:
|
||||
// Called by register_type to also register a tap channel factory.
|
||||
template<typename T>
|
||||
void register_tap_factory(std::size_t capacity = 5) {
|
||||
auto idx = std::type_index(typeid(T));
|
||||
tap_factories_[idx] = [capacity]() -> std::shared_ptr<VChannel> {
|
||||
auto ch = std::make_shared<Channel<T>>(capacity);
|
||||
return std::make_shared<VariantChannel<T, Variant>>(std::move(ch));
|
||||
};
|
||||
}
|
||||
|
||||
private:
|
||||
std::map<std::string, std::shared_ptr<VNode>> nodes_;
|
||||
std::map<std::string, std::vector<std::string>> adj_;
|
||||
std::vector<std::string> topo_;
|
||||
@@ -213,19 +264,27 @@ private:
|
||||
|
||||
std::map<std::type_index, std::function<nb::object(const Variant&)>> to_python_;
|
||||
std::map<std::type_index, std::function<Variant(nb::object)>> from_python_;
|
||||
std::map<std::type_index, std::function<std::shared_ptr<VChannel>()>> tap_factories_;
|
||||
|
||||
// Channel factory: type → function(capacity) → VChannel.
|
||||
// Used both for tap channels (read()) and PyNode input channel creation.
|
||||
std::map<std::type_index,
|
||||
std::function<std::shared_ptr<VChannel>(std::size_t)>> ch_factories_;
|
||||
|
||||
// Friendly name → type_index (e.g. "int" → typeid(int)).
|
||||
std::map<std::string, std::type_index> type_names_;
|
||||
};
|
||||
|
||||
// ── PyNode<Variant> ───────────────────────────────────────────────────────────
|
||||
// A pure-Python processing node. Holds a nanobind callable.
|
||||
// run_loop: pop inputs (release GIL), call Python (acquire GIL), push outputs (release GIL).
|
||||
// run_loop: pop inputs (release GIL), call Python (acquire GIL), push outputs.
|
||||
|
||||
template<typename Variant>
|
||||
class PyNode : public IVariantNode<Variant> {
|
||||
public:
|
||||
using VChannel = IVariantChannel<Variant>;
|
||||
|
||||
using ChannelFactory = std::function<std::shared_ptr<VChannel>(std::size_t capacity)>;
|
||||
using ChannelFactory =
|
||||
std::function<std::shared_ptr<VChannel>(std::size_t capacity)>;
|
||||
|
||||
PyNode(nb::object callable,
|
||||
std::vector<std::type_index> in_types,
|
||||
@@ -264,7 +323,6 @@ public:
|
||||
for (auto& ch : in_channels_) ch->disable();
|
||||
if (thread_.joinable()) {
|
||||
thread_.request_stop();
|
||||
// Release GIL while joining — run_loop may be waiting to acquire it.
|
||||
nb::gil_scoped_release release;
|
||||
thread_.join();
|
||||
}
|
||||
@@ -311,20 +369,17 @@ public:
|
||||
|
||||
private:
|
||||
void run_loop() {
|
||||
// This thread does not hold the GIL. It acquires it only for Python calls.
|
||||
while (!stop_flag_.load(std::memory_order_relaxed)) {
|
||||
try {
|
||||
auto t0 = clock_t::now();
|
||||
|
||||
// Pop all inputs — no GIL needed, these are pure C++ channel ops
|
||||
std::vector<Variant> inputs(in_channels_.size());
|
||||
for (std::size_t i = 0; i < in_channels_.size(); ++i)
|
||||
inputs[i] = in_channels_[i]->pop();
|
||||
|
||||
auto t1 = clock_t::now();
|
||||
auto t1 = clock_t::now();
|
||||
auto cpu0 = NodeStats::cpu_now();
|
||||
|
||||
// Acquire GIL only for the Python call and type conversion
|
||||
std::vector<Variant> outputs;
|
||||
{
|
||||
nb::gil_scoped_acquire acquire;
|
||||
@@ -344,10 +399,9 @@ private:
|
||||
}
|
||||
|
||||
auto cpu1 = NodeStats::cpu_now();
|
||||
auto t2 = clock_t::now();
|
||||
auto t2 = clock_t::now();
|
||||
stats_.record_exec(duration_t(t2 - t1), duration_t(t1 - t0), cpu0, cpu1);
|
||||
|
||||
// Push outputs — no GIL needed
|
||||
for (std::size_t i = 0; i < out_channels_.size(); ++i) {
|
||||
if (out_channels_[i])
|
||||
out_channels_[i]->push(std::move(outputs[i]));
|
||||
@@ -385,9 +439,9 @@ private:
|
||||
NodeStats stats_;
|
||||
};
|
||||
|
||||
// ── register_py_network ───────────────────────────────────────────────────────
|
||||
// Registers PyNetwork<Variant> and PyNode<Variant> with the given nanobind module.
|
||||
// Call once per module, passing the Variant type derived from your registered node types.
|
||||
// ── register_py_network (legacy helper) ───────────────────────────────────────
|
||||
// Registers PyNetwork<Variant> with the given nanobind module.
|
||||
// Prefer bind_network<Registry> from auto_bind.hpp for new code.
|
||||
|
||||
template<typename Variant>
|
||||
void register_py_network(nb::module_& m, const char* class_name = "Network") {
|
||||
@@ -402,7 +456,7 @@ void register_py_network(nb::module_& m, const char* class_name = "Network") {
|
||||
.def("start", &Net::start)
|
||||
.def("stop", &Net::stop)
|
||||
.def("read", &Net::read,
|
||||
nb::arg("node"), nb::arg("out_idx") = 0)
|
||||
nb::arg("node"), nb::arg("out_idx") = std::size_t(0))
|
||||
.def("write", &Net::write,
|
||||
nb::arg("node"), nb::arg("in_idx"), nb::arg("value"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user