Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 82 additions & 31 deletions benchmarks/gpu_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,8 +158,37 @@ def make_iznn_genome(config, genome_id, num_hidden=0):
# Benchmarks
# ---------------------------------------------------------------------------

def fmt_bytes(n):
"""Human-readable byte count."""
if n >= 1 << 20:
return f"{n / (1 << 20):.2f} MB"
if n >= 1 << 10:
return f"{n / (1 << 10):.1f} KB"
return f"{n} B"


def w_h2d_bytes(packed):
"""Bytes crossing the PCIe bus for the weight tensor of a packed pop."""
if packed['W'] is not None:
return packed['W'].nbytes
return packed['edge_index'].nbytes + packed['edge_weight'].nbytes


_HEADER = (f"{'Pop':>8} {'MaxN':>6} {'CPU (s)':>9} {'GPU-d (s)':>10} "
f"{'GPU-s (s)':>10} {'W H2D dense':>12} {'W H2D sparse':>13} "
f"{'xfer cut':>9}")


def _print_row(pop_size, max_nodes, cpu_time, gpu_dense, gpu_sparse,
dense_bytes, sparse_bytes):
reduction = dense_bytes / sparse_bytes if sparse_bytes else float('inf')
print(f"{pop_size:>8d} {max_nodes:>6d} {cpu_time:>9.3f} {gpu_dense:>10.3f} "
f"{gpu_sparse:>10.3f} {fmt_bytes(dense_bytes):>12} "
f"{fmt_bytes(sparse_bytes):>13} {reduction:>8.1f}x")


def benchmark_ctrnn(pop_sizes, num_hidden=3):
"""Benchmark CTRNN CPU vs GPU at various population sizes."""
"""Benchmark CTRNN CPU vs GPU (dense and sparse W upload)."""
from neat.gpu._padding import pack_ctrnn_population
from neat.gpu._cupy_backend import evaluate_ctrnn_batch

Expand All @@ -170,12 +199,12 @@ def benchmark_ctrnn(pop_sizes, num_hidden=3):
input_vals = [0.5, -0.3]
inputs_np = np.tile(np.array(input_vals, dtype=np.float32), (num_steps, 1))

print(f"\n{'='*70}")
print(f"\n{'='*84}")
print(f"CTRNN Benchmark: dt={dt}, t_max={t_max}, num_steps={num_steps}, "
f"hidden_nodes={num_hidden}")
print(f"{'='*70}")
print(f"{'Pop Size':>10} {'Max Nodes':>10} {'CPU (s)':>10} {'GPU (s)':>10} {'Speedup':>10}")
print(f"{'-'*10:>10} {'-'*10:>10} {'-'*10:>10} {'-'*10:>10} {'-'*10:>10}")
print(f"{'='*84}")
print(_HEADER)
print('-' * 84)

for pop_size in pop_sizes:
np.random.seed(42)
Expand All @@ -190,27 +219,36 @@ def benchmark_ctrnn(pop_sizes, num_hidden=3):
net.advance(input_vals, dt, dt)
cpu_time = time.perf_counter() - t0

# GPU timing (include packing + transfer + compute).
# Warmup.
packed = pack_ctrnn_population(genomes, config)
_ = evaluate_ctrnn_batch(packed, inputs_np, dt)
# Warmup both paths (compiles activation + scatter kernels).
_ = evaluate_ctrnn_batch(pack_ctrnn_population(genomes, config),
inputs_np, dt)
_ = evaluate_ctrnn_batch(
pack_ctrnn_population(genomes, config, sparse_upload=True),
inputs_np, dt)
cp.cuda.Stream.null.synchronize()

# GPU timing, dense upload (packing + transfer + compute).
t0 = time.perf_counter()
packed = pack_ctrnn_population(genomes, config)
trajectory = evaluate_ctrnn_batch(packed, inputs_np, dt)
packed_dense = pack_ctrnn_population(genomes, config)
evaluate_ctrnn_batch(packed_dense, inputs_np, dt)
cp.cuda.Stream.null.synchronize()
gpu_time = time.perf_counter() - t0
gpu_dense_time = time.perf_counter() - t0

max_nodes = packed['max_nodes']
speedup = cpu_time / gpu_time if gpu_time > 0 else float('inf')
# GPU timing, sparse upload.
t0 = time.perf_counter()
packed_sparse = pack_ctrnn_population(genomes, config,
sparse_upload=True)
evaluate_ctrnn_batch(packed_sparse, inputs_np, dt)
cp.cuda.Stream.null.synchronize()
gpu_sparse_time = time.perf_counter() - t0

print(f"{pop_size:>10d} {max_nodes:>10d} {cpu_time:>10.3f} {gpu_time:>10.3f} "
f"{speedup:>9.1f}x")
_print_row(pop_size, packed_dense['max_nodes'], cpu_time,
gpu_dense_time, gpu_sparse_time,
w_h2d_bytes(packed_dense), w_h2d_bytes(packed_sparse))


def benchmark_iznn(pop_sizes, num_hidden=3):
"""Benchmark Izhikevich CPU vs GPU at various population sizes."""
"""Benchmark Izhikevich CPU vs GPU (dense and sparse W upload)."""
from neat.gpu._padding import pack_iznn_population
from neat.gpu._cupy_backend import evaluate_iznn_batch

Expand All @@ -221,12 +259,12 @@ def benchmark_iznn(pop_sizes, num_hidden=3):
input_vals = [1.0, 0.5]
inputs_np = np.tile(np.array(input_vals, dtype=np.float32), (num_steps, 1))

print(f"\n{'='*70}")
print(f"\n{'='*84}")
print(f"Izhikevich Benchmark: dt={dt} ms, t_max={t_max} ms, "
f"num_steps={num_steps}, hidden_nodes={num_hidden}")
print(f"{'='*70}")
print(f"{'Pop Size':>10} {'Max Nodes':>10} {'CPU (s)':>10} {'GPU (s)':>10} {'Speedup':>10}")
print(f"{'-'*10:>10} {'-'*10:>10} {'-'*10:>10} {'-'*10:>10} {'-'*10:>10}")
print(f"{'='*84}")
print(_HEADER)
print('-' * 84)

for pop_size in pop_sizes:
np.random.seed(42)
Expand All @@ -242,25 +280,38 @@ def benchmark_iznn(pop_sizes, num_hidden=3):
net.advance(dt)
cpu_time = time.perf_counter() - t0

# GPU timing.
packed = pack_iznn_population(genomes, config)
_ = evaluate_iznn_batch(packed, inputs_np, dt, num_steps)
# Warmup both paths.
_ = evaluate_iznn_batch(pack_iznn_population(genomes, config),
inputs_np, dt, num_steps)
_ = evaluate_iznn_batch(
pack_iznn_population(genomes, config, sparse_upload=True),
inputs_np, dt, num_steps)
cp.cuda.Stream.null.synchronize()

# GPU timing, dense upload.
t0 = time.perf_counter()
packed = pack_iznn_population(genomes, config)
trajectory = evaluate_iznn_batch(packed, inputs_np, dt, num_steps)
packed_dense = pack_iznn_population(genomes, config)
evaluate_iznn_batch(packed_dense, inputs_np, dt, num_steps)
cp.cuda.Stream.null.synchronize()
gpu_time = time.perf_counter() - t0
gpu_dense_time = time.perf_counter() - t0

max_nodes = packed['max_nodes']
speedup = cpu_time / gpu_time if gpu_time > 0 else float('inf')
# GPU timing, sparse upload.
t0 = time.perf_counter()
packed_sparse = pack_iznn_population(genomes, config,
sparse_upload=True)
evaluate_iznn_batch(packed_sparse, inputs_np, dt, num_steps)
cp.cuda.Stream.null.synchronize()
gpu_sparse_time = time.perf_counter() - t0

print(f"{pop_size:>10d} {max_nodes:>10d} {cpu_time:>10.3f} {gpu_time:>10.3f} "
f"{speedup:>9.1f}x")
_print_row(pop_size, packed_dense['max_nodes'], cpu_time,
gpu_dense_time, gpu_sparse_time,
w_h2d_bytes(packed_dense), w_h2d_bytes(packed_sparse))


if __name__ == '__main__':
pop_sizes = [100, 500, 1000]
benchmark_ctrnn(pop_sizes)
# Larger networks: the dense W tensor scales with max_nodes^2, the sparse
# edge list only with connection count — this run shows the transfer win.
benchmark_ctrnn(pop_sizes, num_hidden=32)
benchmark_iznn(pop_sizes)
80 changes: 72 additions & 8 deletions neat/gpu/_cupy_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,77 @@
return cp.RawKernel(_ACTIVATION_KERNEL_CODE, 'apply_activation')


# ---------------------------------------------------------------------------
# COO-edge scatter kernel (sparse W upload)
# ---------------------------------------------------------------------------
# Scatters a COO edge list (genome_idx, dst, src, weight) into a
# zero-initialized dense W [N, M, M] on the device. Used when the population
# was packed with sparse_upload=True: only ~16 bytes per enabled connection
# cross the PCIe bus instead of the full N*M*M*4-byte dense tensor.

_SCATTER_KERNEL_CODE = '''
extern "C" __global__
void scatter_edges(
const int* __restrict__ edge_index, // [E, 3] rows: (genome, dst, src)
const float* __restrict__ edge_weight, // [E]
float* __restrict__ W, // [N*M*M], zero-initialized
int num_edges,
int M
) {
int i = blockDim.x * blockIdx.x + threadIdx.x;
if (i >= num_edges) return;

// 64-bit offset: N*M*M can exceed 2^31 for large populations.
long long g = edge_index[3 * i];
long long dst = edge_index[3 * i + 1];
long long src = edge_index[3 * i + 2];
W[(g * M + dst) * M + src] = edge_weight[i];
}
'''


def _get_scatter_kernel():
"""Compile and cache the edge scatter kernel."""
cp = _import_cupy()
return cp.RawKernel(_SCATTER_KERNEL_CODE, 'scatter_edges')


def _upload_W(cp, packed):

Check warning on line 130 in neat/gpu/_cupy_backend.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "_upload_W" to match the regular expression ^[a-z_][a-z0-9_]*$.

See more on https://sonarcloud.io/project/issues?id=CodeReclaimers_neat-python&issues=AZ80aVIMgn42AMKbixyB&open=AZ80aVIMgn42AMKbixyB&pullRequest=294
"""
Materialize the dense weight tensor W [N, M, M] on the device.

Dense-packed populations (packed['W'] is an ndarray) transfer it as-is.
Sparse-packed populations (packed['W'] is None) transfer only the COO
edge arrays and scatter them into a device-side zero tensor.
"""
if packed['W'] is not None:
return cp.asarray(packed['W'])

N = packed['num_genomes']
M = packed['max_nodes']
W = cp.zeros((N, M, M), dtype=cp.float32)

num_edges = packed['edge_index'].shape[0]
if num_edges:
edge_index = cp.asarray(packed['edge_index'])
edge_weight = cp.asarray(packed['edge_weight'])
kernel = _get_scatter_kernel()
block_size = 256
grid_size = (num_edges + block_size - 1) // block_size
kernel((grid_size,), (block_size,),
(edge_index, edge_weight, W, num_edges, M))
return W


def evaluate_ctrnn_batch(packed, inputs_cpu, dt):
"""
Run batched CTRNN simulation on GPU using exponential Euler integration.

Parameters
----------
packed : dict
Output of pack_ctrnn_population(). NumPy arrays on CPU.
Output of pack_ctrnn_population(), dense or sparse_upload=True.
NumPy arrays on CPU.
inputs_cpu : ndarray [num_steps, num_inputs] or [num_steps, N, num_inputs]
Precomputed input trajectory. If 2-D, broadcast across population.
dt : float
Expand All @@ -113,14 +176,14 @@
cp = _import_cupy()
np = _import_numpy()

N = packed['W'].shape[0]
N = packed['num_genomes']
M = packed['max_nodes']
num_inputs = packed['num_inputs']
num_outputs = packed['num_outputs']
num_steps = inputs_cpu.shape[0]

# Transfer parameters to GPU.
W = cp.asarray(packed['W']) # [N, M, M]
# Transfer parameters to GPU (W: dense copy or sparse scatter).
W = _upload_W(cp, packed) # [N, M, M]
bias = cp.asarray(packed['bias']) # [N, M]
response = cp.asarray(packed['response']) # [N, M]
tau = cp.asarray(packed['tau']) # [N, M]
Expand Down Expand Up @@ -196,7 +259,8 @@
Parameters
----------
packed : dict
Output of pack_iznn_population(). NumPy arrays on CPU.
Output of pack_iznn_population(), dense or sparse_upload=True.
NumPy arrays on CPU.
inputs_cpu : ndarray [num_steps, num_inputs] or [num_steps, N, num_inputs]
Precomputed input trajectory.
dt : float
Expand All @@ -212,13 +276,13 @@
cp = _import_cupy()
np = _import_numpy()

N = packed['W'].shape[0]
N = packed['num_genomes']
M = packed['max_nodes']
num_inputs = packed['num_inputs']
num_outputs = packed['num_outputs']

# Transfer to GPU.
W = cp.asarray(packed['W']) # [N, M, M]
# Transfer to GPU (W: dense copy or sparse scatter).
W = _upload_W(cp, packed) # [N, M, M]
bias = cp.asarray(packed['bias']) # [N, M]
a = cp.asarray(packed['a']) # [N, M]
b = cp.asarray(packed['b']) # [N, M]
Expand Down
Loading