<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>GeNN (Software Developer Blog)</title><link>http://genn-team.github.io/</link><description>Welcome to GeNN</description><atom:link href="http://genn-team.github.io/rss.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2025 &lt;a href="mailto:t.nowotny@sussex.ac.uk"&gt;GeNN Team&lt;/a&gt; </copyright><lastBuildDate>Wed, 17 Sep 2025 16:55:29 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Conda packaging for GeNN</title><link>http://genn-team.github.io/posts/conda-packaging-for-genn.html</link><dc:creator>Agrim Patil</dc:creator><description>&lt;hr&gt;
&lt;h3&gt;📂 Project Repository&lt;/h3&gt;
&lt;p&gt;🔗 &lt;a href="https://github.com/Agrim-P777/Conda-package-GeNN"&gt;Conda-package-GeNN&lt;/a&gt;&lt;br&gt;
This repository contains &lt;em&gt;all the code, packaging recipes, and documentation&lt;/em&gt; developed during my Google Summer of Code project.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;📑 Table of Contents&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;🌍 &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-google-summer-of-code-gsoc"&gt;Google Summer of Code (GSoC)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🧠 &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-about-incf"&gt;About INCF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚡ &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-about-genn"&gt;About GeNN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;❓ &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-problem-statement"&gt;Problem Statement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-deliverables"&gt;Deliverables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🎮 &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-rise-of-cuda-in-neural-simulations"&gt;Rise of CUDA in Neural Simulations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-why-conda-and-not-pypi"&gt;Why Conda (and not PyPI)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🏗️ &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#%EF%B8%8F-package-architecture"&gt;Package Architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚔️ &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#%EF%B8%8F-challenges-faced-and-solutions"&gt;Challenges Faced and Solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🌀 &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-challenge-1-transition-from-cuda-12x-to-cuda-12x"&gt;Challenge 1: Transition from CUDA &amp;lt;12.x to CUDA ≥12.x&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚔️ &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#%EF%B8%8F-challenge-2-setting-cuda_path-after-installation"&gt;Challenge 2: Setting CUDA_PATH After Installation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚔️ &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#%EF%B8%8F-challenge-3-moving-windows-build-to-nmake--msbuild"&gt;Challenge 3: Moving Windows Build to NMake + MSBuild&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;⚔️ &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#%EF%B8%8F-challenge-4-fixing-macos-dylib-handling-in-pygenn-cpu"&gt;Challenge 4: Fixing macOS .dylib Handling in pygenn-cpu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;📦 &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-conda-forge-packages"&gt;Conda-Forge Packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;🌟 &lt;a href="http://genn-team.github.io/posts/conda-packaging-for-genn.html#-impact-of-the-package"&gt;Impact of the Package&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🌍 Google Summer of Code (GSoC)&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://summerofcode.withgoogle.com/"&gt;Google Summer of Code (GSoC)&lt;/a&gt; is an annual global program focused on bringing new contributors into open source software development.&lt;br&gt;
Contributors work with open source organizations under the guidance of mentors to learn, code, and make impactful contributions during the summer.&lt;/p&gt;
&lt;h4&gt;📊 GSoC 2025 Highlights&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;15,240 applicants&lt;/strong&gt; from &lt;strong&gt;130 countries&lt;/strong&gt; submitted &lt;strong&gt;23,559 proposals&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;185 mentoring organizations&lt;/strong&gt; selected &lt;strong&gt;1,272 contributors&lt;/strong&gt; from &lt;strong&gt;68 countries&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;66.3% of contributors&lt;/strong&gt; had &lt;em&gt;no prior open source experience&lt;/em&gt;, showing GSoC’s accessibility  &lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;three-week Community Bonding period&lt;/strong&gt; helps contributors and mentors plan and get oriented before coding  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🔗 &lt;a href="https://opensource.googleblog.com/2025/05/gsoc-2025-we-have-our-contributors.html"&gt;Read more on the official announcement&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;🧠 About INCF&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.incf.org/"&gt;&lt;img alt="INCF" src="https://gist.github.com/user-attachments/assets/3cce81f1-081a-4d65-a0c0-42321f10325a"&gt;&lt;/a&gt;&lt;br&gt;
The &lt;a href="https://www.incf.org/"&gt;International Neuroinformatics Coordinating Facility (INCF)&lt;/a&gt; is an open and FAIR (Findable, Accessible, Interoperable, and Reusable) neuroscience standards organization.&lt;br&gt;
Launched in 2005 through a proposal from the OECD Global Science Forum, INCF’s mission is to make neuroscience data and knowledge &lt;strong&gt;globally shareable and reusable&lt;/strong&gt;.&lt;/p&gt;
&lt;h4&gt;🌐 Impact on Society&lt;/h4&gt;
&lt;p&gt;By developing community-driven standards and tools for data sharing, analysis, modeling, and simulation, INCF:
- Promotes &lt;strong&gt;collaboration&lt;/strong&gt; across international neuroscience communities&lt;br&gt;
- Enables &lt;strong&gt;reproducible and scalable research&lt;/strong&gt;&lt;br&gt;
- Accelerates &lt;strong&gt;discoveries in brain science&lt;/strong&gt;&lt;br&gt;
- Supports better understanding of brain function in both &lt;strong&gt;health and disease&lt;/strong&gt;  &lt;/p&gt;
&lt;p&gt;Through these efforts, INCF helps build a more open scientific ecosystem, ultimately contributing to advances in healthcare, mental health, and neurological research worldwide.&lt;/p&gt;
&lt;h3&gt;⚡ About GeNN&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://genn-team.github.io/"&gt;&lt;img alt="GeNN" src="https://gist.github.com/user-attachments/assets/16386ccf-35dd-4ae5-93fa-fd662fdce122"&gt;&lt;/a&gt;&lt;br&gt;
The &lt;a href="https://genn-team.github.io/"&gt;GPU-enhanced Neuronal Networks (GeNN)&lt;/a&gt; project is a code generation framework designed to accelerate the simulation of spiking neural networks (SNNs) using GPUs.  &lt;/p&gt;
&lt;h4&gt;🔬 Role in Neuroscience&lt;/h4&gt;
&lt;p&gt;GeNN plays a crucial role in computational neuroscience by:
- Enabling &lt;strong&gt;fast and efficient simulation&lt;/strong&gt; of large-scale spiking neural networks&lt;br&gt;
- Allowing researchers to &lt;strong&gt;prototype and test brain-inspired models&lt;/strong&gt; at unprecedented scales&lt;br&gt;
- Supporting &lt;strong&gt;reproducibility and standardization&lt;/strong&gt; in neural simulations&lt;br&gt;
- Bridging the gap between &lt;strong&gt;biological realism and computational efficiency&lt;/strong&gt;  &lt;/p&gt;
&lt;p&gt;Through its GPU acceleration, GeNN empowers neuroscientists to explore complex models of brain function that would otherwise be computationally prohibitive.&lt;/p&gt;
&lt;h3&gt;❓ Problem Statement&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://genn-team.github.io/"&gt;GeNN&lt;/a&gt; is a &lt;strong&gt;C++ library&lt;/strong&gt; that generates code for efficiently simulating &lt;strong&gt;Spiking Neural Networks (SNNs)&lt;/strong&gt; using GPUs.&lt;br&gt;
To compile the generated code, GeNN requires a &lt;strong&gt;C++ compiler&lt;/strong&gt; and development versions of backend dependencies such as &lt;strong&gt;CUDA&lt;/strong&gt;.  &lt;/p&gt;
&lt;p&gt;Currently, this means GeNN must be &lt;strong&gt;installed from source&lt;/strong&gt;, which can be a barrier for many potential users:
- Researchers may not have the right compiler or CUDA version installed
- Installation errors can take hours to resolve
- New users may be discouraged before even running their first simulation&lt;/p&gt;
&lt;h4&gt;🎯 Project Goal&lt;/h4&gt;
&lt;p&gt;For this project, I aimed to develop a &lt;strong&gt;Conda (Forge) package&lt;/strong&gt; for GeNN which:
- Handles the installation of all required dependencies (C++, CUDA, libraries)
- Provides pre-built binaries for Linux, Windows, and macOS
- Makes installation as simple as:&lt;/p&gt;
&lt;p&gt;```bash
  conda install -c conda-forge pygenn-cpu   # CPU-only
  conda install -c conda-forge pygenn-cuda  # CUDA-enabled&lt;/p&gt;
&lt;h4&gt;📦 Deliverables&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;✅ Conda-Forge recipes for both &lt;strong&gt;CPU&lt;/strong&gt; and &lt;strong&gt;CUDA&lt;/strong&gt; variants of GeNN&lt;/li&gt;
&lt;li&gt;✅ User documentation and installation instructions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🎮 Rise of CUDA in Neural Simulations&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://developer.nvidia.com/cuda-toolkit"&gt;&lt;img alt="NVIDIA" src="https://gist.github.com/user-attachments/assets/c8d2b869-7d5b-47a1-91f3-4acb862e5e2b"&gt;&lt;/a&gt;  &lt;/p&gt;
&lt;p&gt;The introduction of &lt;strong&gt;CUDA (Compute Unified Device Architecture)&lt;/strong&gt; by NVIDIA revolutionized the way scientists and engineers simulate neural networks.  &lt;/p&gt;
&lt;h4&gt;🚀 Why CUDA Matters&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Provides &lt;strong&gt;massive parallelism&lt;/strong&gt; by leveraging thousands of GPU cores  &lt;/li&gt;
&lt;li&gt;Accelerates &lt;strong&gt;matrix operations&lt;/strong&gt; and &lt;strong&gt;synaptic updates&lt;/strong&gt; critical for spiking neural networks  &lt;/li&gt;
&lt;li&gt;Reduces simulation times from &lt;strong&gt;hours or days to minutes or seconds&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;Allows scaling to &lt;strong&gt;millions of neurons and synapses&lt;/strong&gt; in realistic brain models  &lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;🧩 Impact on Neuroscience&lt;/h4&gt;
&lt;p&gt;By harnessing CUDA, researchers can:
- Explore &lt;strong&gt;biologically detailed models&lt;/strong&gt; of neural circuits&lt;br&gt;
- Run &lt;strong&gt;real-time simulations&lt;/strong&gt; for robotics and brain-inspired AI&lt;br&gt;
- Investigate complex dynamics of the brain that were previously infeasible due to computational limits  &lt;/p&gt;
&lt;p&gt;In short, CUDA has been a &lt;strong&gt;key enabler&lt;/strong&gt; in advancing computational neuroscience and the adoption of frameworks like &lt;strong&gt;GeNN&lt;/strong&gt;.&lt;/p&gt;
&lt;h3&gt;📦 Why Conda (and not PyPI)&lt;/h3&gt;
&lt;p&gt;We chose &lt;strong&gt;Conda&lt;/strong&gt; because our package is not just Python — it also includes a &lt;strong&gt;C++ backend and CUDA code&lt;/strong&gt;.  &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Conda can package &lt;strong&gt;non-Python dependencies&lt;/strong&gt; (C++, CUDA, compilers, system libraries), while PyPI is limited to Python-only distributions.  &lt;/li&gt;
&lt;li&gt;With Conda we can &lt;strong&gt;pin CUDA versions and compilers&lt;/strong&gt;, ensuring compatibility across Linux, Windows, and macOS.  &lt;/li&gt;
&lt;li&gt;This makes Conda the better choice for distributing GPU-accelerated scientific software like &lt;strong&gt;GeNN&lt;/strong&gt;, where reproducibility and native dependencies are critical.  &lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🏗️ Package Architecture&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://conda-forge.org/"&gt;&lt;img alt="Conda-Forge" src="https://gist.github.com/user-attachments/assets/31591c25-0ccd-4147-ad2e-ed39d6964785"&gt;&lt;/a&gt;  &lt;/p&gt;
&lt;p&gt;We designed the package to provide &lt;strong&gt;two build variants&lt;/strong&gt; of GeNN:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;CPU-only&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;Lightweight build that works without CUDA  &lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Useful for users who want to experiment with spiking neural networks on any system  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;CUDA-enabled&lt;/strong&gt;  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Full GPU acceleration using modular CUDA packages  &lt;/li&gt;
&lt;li&gt;Ideal for large-scale neuroscience simulations  &lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;📂 Structure&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Separate Conda recipes: &lt;code&gt;pygenn-cpu&lt;/code&gt; and &lt;code&gt;pygenn-cuda&lt;/code&gt;  &lt;/li&gt;
&lt;li&gt;Each recipe pins Python, NumPy ABI, and (for CUDA builds) modular CUDA components like &lt;code&gt;cuda-nvcc&lt;/code&gt;, &lt;code&gt;cuda-cudart&lt;/code&gt;, and &lt;code&gt;cuda-libraries&lt;/code&gt;  &lt;/li&gt;
&lt;li&gt;Shared test suite ensures both variants behave consistently  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This dual-architecture approach makes GeNN more &lt;strong&gt;accessible and reproducible&lt;/strong&gt;, whether on laptops or GPU clusters.  &lt;/p&gt;
&lt;p&gt;🔗 &lt;a href="https://github.com/Agrim-P777/Conda-package-GeNN/wiki/05.-GeNN-%E2%80%90-Conda-Package-Structure:-CPU%E2%80%90Only-and-CUDA%E2%80%90Enabled"&gt;Read more on the detailed package structure&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;⚔️ Challenges Faced and Solutions&lt;/h3&gt;
&lt;h4&gt;🌀 Challenge 1: Transition from CUDA &amp;lt;12.x to CUDA ≥12.x&lt;/h4&gt;
&lt;p&gt;Initially, our package was built for &lt;strong&gt;CUDA 11.7&lt;/strong&gt;, which used a &lt;strong&gt;monolithic toolkit package&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;👉 &lt;a href="https://github.com/Agrim-P777/Conda-package-GeNN/blob/main/pygenn-linux-cuda11.7/meta.yaml"&gt;Example: CUDA 11.7 recipe&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;However, starting with &lt;strong&gt;CUDA 12.x&lt;/strong&gt;, Conda-Forge adopted a &lt;strong&gt;modular CUDA packaging&lt;/strong&gt; system:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instead of a single &lt;code&gt;cudatoolkit&lt;/code&gt; package&lt;/li&gt;
&lt;li&gt;CUDA is split into components like &lt;code&gt;cuda-nvcc&lt;/code&gt;, &lt;code&gt;cuda-cudart&lt;/code&gt;, &lt;code&gt;cuda-libraries&lt;/code&gt;, &lt;code&gt;cuda-libraries-dev&lt;/code&gt;, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🔗 &lt;a href="https://github.com/Agrim-P777/Conda-package-GeNN/wiki/06.-Understanding-CUDA-Packaging-in-Conda%E2%80%90Forge:-Pre%E2%80%9012-vs-Post%E2%80%9012-Versions"&gt;Detailed explanation: Pre-12 vs Post-12 CUDA packaging&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;✅ Our Solution&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Migrated the recipe to &lt;strong&gt;modular CUDA dependencies&lt;/strong&gt; in &lt;code&gt;meta.yaml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Explicitly pinned the CUDA version with:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yaml
- cuda-version =={{ cuda_version }}
- cuda-nvcc {{ cuda_nvcc }}
- cuda-cudart {{ cuda_cudart }}
- cuda-libraries {{ cuda_libraries }}
- cuda-libraries-dev {{ cuda_libraries_dev }}&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ensured compatibility across &lt;strong&gt;Linux, Windows, and macOS&lt;/strong&gt; by adjusting the build matrix and using Conda’s modular CUDA toolchain.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This transition was essential to keep the package &lt;strong&gt;future-proof and aligned&lt;/strong&gt; with Conda-Forge’s evolving CUDA ecosystem.&lt;/p&gt;
&lt;h4&gt;⚔️ Challenge 2: Setting &lt;code&gt;CUDA_PATH&lt;/code&gt; After Installation&lt;/h4&gt;
&lt;p&gt;During testing, we discovered that after installing the CUDA-enabled package,&lt;/p&gt;
&lt;p&gt;the &lt;strong&gt;&lt;code&gt;CUDA_PATH&lt;/code&gt; environment variable&lt;/strong&gt; was &lt;strong&gt;not automatically set&lt;/strong&gt; in the Conda environment.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;This caused issues on both &lt;strong&gt;Linux&lt;/strong&gt; and &lt;strong&gt;Windows&lt;/strong&gt;, where users needed &lt;code&gt;CUDA_PATH&lt;/code&gt; for compiling and running GeNN models.&lt;/li&gt;
&lt;li&gt;Without it, the CUDA backend could not be located properly by the build system.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🔗 &lt;a href="https://github.com/Agrim-P777/Conda-package-GeNN/wiki/08.-Including-a-post%E2%80%90link.sh-script-in-the-Conda-Package"&gt;Reference: post-link script design&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;✅ Our Solution&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Added &lt;strong&gt;&lt;code&gt;post-link.sh&lt;/code&gt;&lt;/strong&gt; (Linux/macOS) and &lt;strong&gt;&lt;code&gt;post-link.bat&lt;/code&gt;&lt;/strong&gt; (Windows) scripts to the recipe.&lt;/li&gt;
&lt;li&gt;These scripts:&lt;ul&gt;
&lt;li&gt;Notify users that they must export or set &lt;code&gt;CUDA_PATH&lt;/code&gt; in their shell session&lt;/li&gt;
&lt;li&gt;Provide clear guidance on how to configure it (&lt;code&gt;export CUDA_PATH=$CONDA_PREFIX&lt;/code&gt; on Linux/macOS, &lt;code&gt;set CUDA_PATH=%CONDA_PREFIX%\\Library&lt;/code&gt; on Windows)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example &lt;code&gt;post-link.sh&lt;/code&gt; Script&lt;/strong&gt;&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"============================================"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PyGeNN CUDA backend installed successfully!"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"To enable CUDA support, set the environment variable:"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"    export CUDA_PATH=&lt;/span&gt;&lt;span class="nv"&gt;$CONDA_PREFIX&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Alternatively, if you have a system-wide CUDA installation:"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"    export CUDA_PATH=/usr/local/cuda-12.x"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PyGeNN will automatically use CUDA_PATH if set; otherwise, you may"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"need to manually configure it for certain use cases."&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"============================================"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This ensures users are explicitly informed about the required step, making the installation process &lt;strong&gt;clearer and less error-prone&lt;/strong&gt;.&lt;/p&gt;
&lt;h4&gt;⚔️ Challenge 3: Moving Windows Build to NMake + MSBuild&lt;/h4&gt;
&lt;p&gt;Originally, the Windows build system relied only on &lt;strong&gt;MSBuild&lt;/strong&gt;, which was insufficient to support conda pacakge's 
GeNN’s requirement for &lt;strong&gt;runtime code compilation&lt;/strong&gt; of models.  &lt;/p&gt;
&lt;h4&gt;✅ Our Solution&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Migrated the Windows backend to a hybrid &lt;strong&gt;NMake + MSBuild&lt;/strong&gt; system.  &lt;/li&gt;
&lt;li&gt;Benefits of this change:&lt;/li&gt;
&lt;li&gt;Enabled &lt;strong&gt;runtime compilation&lt;/strong&gt; of CUDA kernels on Windows  &lt;/li&gt;
&lt;li&gt;Added &lt;strong&gt;robust CUDA path management&lt;/strong&gt;, ensuring builds work with Conda’s modular CUDA layout  &lt;/li&gt;
&lt;li&gt;Standardized the use of &lt;strong&gt;&lt;code&gt;CUDA_LIBRARY_PATH&lt;/code&gt;&lt;/strong&gt; across Windows environments for consistency  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This migration improved reliability and made the Windows build &lt;strong&gt;much closer to Linux in flexibility&lt;/strong&gt;,&lt;br&gt;
while also aligning with Conda’s CUDA packaging best practices.  &lt;/p&gt;
&lt;p&gt;🔗 &lt;a href="https://github.com/genn-team/genn/pull/705"&gt;My Pull Request #705 – robust CUDA lib path resolution for Conda &amp;amp; system installs&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;⚔️ Challenge 4: Fixing macOS &lt;code&gt;.dylib&lt;/code&gt; Handling in &lt;code&gt;pygenn-cpu&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;When building the &lt;strong&gt;CPU-only PyGeNN package&lt;/strong&gt; on macOS, we encountered an issue where&lt;br&gt;
the required &lt;strong&gt;dynamic libraries (&lt;code&gt;.dylib&lt;/code&gt;)&lt;/strong&gt; were &lt;strong&gt;not being copied correctly&lt;/strong&gt; into the installed package directory.&lt;br&gt;
This caused runtime errors where Python could not locate GeNN’s backend libraries.&lt;/p&gt;
&lt;h4&gt;✅ Our Solution (My PR 🔧)&lt;/h4&gt;
&lt;p&gt;I submitted &lt;a href="https://github.com/genn-team/genn/pull/707"&gt;PR #707&lt;/a&gt; to fix the &lt;strong&gt;macOS library handling&lt;/strong&gt; in &lt;code&gt;setup.py&lt;/code&gt;.&lt;br&gt;
Key technical improvements included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dynamic Library Discovery&lt;/strong&gt;  &lt;/li&gt;
&lt;li&gt;Updated &lt;code&gt;setup.py&lt;/code&gt; to explicitly find GeNN’s &lt;code&gt;.dylib&lt;/code&gt; artifacts generated during the build process.  &lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Ensured both the &lt;strong&gt;core &lt;code&gt;libgenn_dynamic.dylib&lt;/code&gt;&lt;/strong&gt; and the &lt;strong&gt;CPU backend libraries&lt;/strong&gt; were properly detected.  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Correct Copy into &lt;code&gt;site-packages&lt;/code&gt;&lt;/strong&gt;  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Added logic to copy these &lt;code&gt;.dylib&lt;/code&gt; files into the final &lt;code&gt;pygenn&lt;/code&gt; installation directory under &lt;code&gt;site-packages&lt;/code&gt;.  &lt;/li&gt;
&lt;li&gt;
&lt;p&gt;This guarantees the Python extension modules can locate their linked dynamic libraries at runtime.  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;macOS Loader Path Fixes&lt;/strong&gt;  &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Adjusted the &lt;code&gt;install_name&lt;/code&gt; handling so that macOS’s runtime linker resolves the &lt;code&gt;.dylib&lt;/code&gt; files correctly.  &lt;/li&gt;
&lt;li&gt;Prevented the “image not found” errors that occurred when relocating the package to a Conda environment.  &lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;🔬 Impact&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Resolved &lt;strong&gt;import-time failures&lt;/strong&gt; on macOS for the &lt;code&gt;pygenn-cpu&lt;/code&gt; package.  &lt;/li&gt;
&lt;li&gt;Improved &lt;strong&gt;cross-platform parity&lt;/strong&gt;, since Linux &lt;code&gt;.so&lt;/code&gt; handling was already stable.  &lt;/li&gt;
&lt;li&gt;Made the CPU-only build truly &lt;strong&gt;portable&lt;/strong&gt; across Conda environments on macOS.  &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🔗 &lt;a href="https://github.com/genn-team/genn/pull/707"&gt;My Pull Request #707 – macOS &lt;code&gt;.dylib&lt;/code&gt; fix in setup.py&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;📦 Conda-Forge Packages&lt;/h3&gt;
&lt;p&gt;After resolving build system and packaging challenges, we contributed to the &lt;strong&gt;official Conda-Forge recipes&lt;/strong&gt; for PyGeNN.&lt;/p&gt;
&lt;h4&gt;🚀 Published Packages&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;pygenn-cuda&lt;/strong&gt; → &lt;a href="https://github.com/conda-forge/staged-recipes/pull/30899"&gt;staged-recipes PR #30899&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;GPU-accelerated build with modular CUDA support&lt;/li&gt;
&lt;li&gt;Targets Linux and Windows with reproducible CUDA environments&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;pygenn-cpu&lt;/strong&gt; → &lt;a href="https://github.com/conda-forge/staged-recipes/pull/30907"&gt;staged-recipes PR #30907&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;Lightweight CPU-only build&lt;/li&gt;
&lt;li&gt;Cross-platform support (Linux, Windows, macOS) without CUDA dependency&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;🌐 Impact&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Brought &lt;strong&gt;PyGeNN to the Conda-Forge ecosystem&lt;/strong&gt;, making installation as simple as:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;bash
conda install -c conda-forge pygenn-cpu   # CPU-only
conda install -c conda-forge pygenn-cuda  # CUDA-enabled&lt;/code&gt;
- Improved &lt;strong&gt;discoverability, reproducibility, and accessibility&lt;/strong&gt; for neuroscience researchers and developers worldwide.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;🌟 Impact of the Package&lt;/h3&gt;
&lt;p&gt;Before our Conda-Forge packages, users had to &lt;strong&gt;install GeNN from source&lt;/strong&gt;:&lt;br&gt;
- Clone the repository&lt;br&gt;
- Configure compilers and CUDA toolchains manually&lt;br&gt;
- Build the C++ backend&lt;br&gt;
- Troubleshoot platform-specific errors (Linux, Windows, macOS)  &lt;/p&gt;
&lt;p&gt;This process was &lt;strong&gt;time-consuming and error-prone&lt;/strong&gt;, often taking &lt;strong&gt;hours&lt;/strong&gt; for new users.&lt;/p&gt;
&lt;h4&gt;🚀 Improvements with Conda Packages&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Installation reduced to a &lt;strong&gt;single command&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;bash
  conda install -c conda-forge pygenn-cpu   # CPU-only
  conda install -c conda-forge pygenn-cuda  # CUDA-enabled&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No manual compilation&lt;/strong&gt; needed — all binaries are pre-built for the target platform&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-platform availability&lt;/strong&gt;: Linux, Windows, and macOS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pinned toolchains and CUDA versions&lt;/strong&gt; ensure reproducibility and stability&lt;/li&gt;
&lt;li&gt;Eliminates setup barriers, letting researchers focus on &lt;strong&gt;science, not build systems&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;🔬 Impact on Researchers&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Decreased installation time from &lt;strong&gt;hours → minutes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Made GeNN accessible to &lt;strong&gt;a wider audience&lt;/strong&gt;, including those without deep build/DevOps expertise&lt;/li&gt;
&lt;li&gt;Strengthened the reliability of &lt;strong&gt;neuroscience workflows&lt;/strong&gt; by providing reproducible environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short, this packaging effort turned GeNN from a &lt;strong&gt;complex source-based project&lt;/strong&gt; into an &lt;strong&gt;accessible plug-and-play library&lt;/strong&gt; for the neuroscience community!&lt;/p&gt;</description><category>GSOC</category><guid>http://genn-team.github.io/posts/conda-packaging-for-genn.html</guid><pubDate>Wed, 17 Sep 2025 16:54:15 GMT</pubDate></item><item><title>Developing an ISPC Backend for GeNN: Bridging GPU and CPU Performance for Neural Network Simulations</title><link>http://genn-team.github.io/posts/developing-an-ispc-backend-for-genn-bridging-gpu-and-cpu-performance-for-neural-network-simulations.html</link><dc:creator>Jash Vora</dc:creator><description>&lt;h3&gt;Abstract&lt;/h3&gt;
&lt;p&gt;This report presents the development of an Intel SPMD Program Compiler (ISPC) backend for GeNN (GPU-Enhanced Neuronal Networks), a code generation framework for simulating spiking neural networks . The primary goal of this project was to reduce the performance gap between single-threaded CPU implementations and GPU-accelerated simulations by exploiting the SIMD (Single Instruction, Multiple Data) parallelism available in modern processors.&lt;/p&gt;
&lt;p&gt;The project involved the development of a ISPC-based code generation backend within GeNN. This included kernel generation for neuron updates, synaptic processing, and custom model operations. Benchmarking and performance evaluation demonstrate that the ISPC backend achieves considerable speedups over the single-threaded CPU implementations, while retaining full compatibility with existing GeNN models. At the same time, it is easier to use and is broadly accessibly compared to GPU solutions.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Introduction &amp;amp; Need for the Project&lt;/h3&gt;
&lt;h4&gt;Background on Neural Simulations and GeNN&lt;/h4&gt;
&lt;p&gt;&lt;img alt="Comparison of Artificial Neural Networks and Spiking Neural Networks" src="http://genn-team.github.io/images/snn.jpg"&gt; 
&lt;em&gt;Figure 1: Comparison between traditional Artificial Neural Networks (ANNs) and Spiking Neural Networks (SNNs), illustrating the difference in information processing and representation.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Traditional artificial neural networks (ANNs), as shown in panel (a), process real-valued inputs and outputs in a series of layers. Each neuron produces a continuous activation value that is passed forward through weighted connections.&lt;/p&gt;
&lt;p&gt;Panel (b) illustrates how these activations are typically represented as real numbers, such as 0.3 or 0.8, which are updated every time step during training or inference.&lt;/p&gt;
&lt;p&gt;Spiking neural networks (SNNs), shown in panel (c), work differently. Instead of passing continuous values, neurons communicate through discrete spikes that occur at particular points in time. Information is encoded in the timing and frequency of these spikes, making SNNs closer to how biological neurons operate. This event-driven style of computation can be much more energy efficient, since neurons are mostly idle and only update when spikes occur.&lt;/p&gt;
&lt;p&gt;GeNN (GPU-enhanced Neuronal Networks) is a framework designed to accelerate simulations of spiking neural networks. It uses code generation to produce optimized kernels for different backends, such as GPUs and CPUs. This makes it possible for researchers to test large-scale SNN models efficiently, without having to write low-level code themselves.&lt;/p&gt;
&lt;h4&gt;Motivation for ISPC Backend&lt;/h4&gt;
&lt;p&gt;The need for an ISPC backend arises from several limitations in the existing GeNN ecosystem:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hardware Accessibility&lt;/strong&gt;: Not all users have access to high-end GPUs, limiting the adoption of GeNN's GPU-accelerated features. ISPC compiler is also easier to setup than CUDA.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Performance Gap&lt;/strong&gt;: Single-threaded CPU implementations often exhibit poor performance compared to GPU versions, creating a significant dip in performance for users without GPU access.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;SIMD Underutilization&lt;/strong&gt;: Modern CPUs feature powerful SIMD instruction sets (SSE, AVX, AVX-512) that remain largely untapped in traditional scalar CPU implementations. Using certain keywords in the code could give major performance boosts in computations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cross-Platform Portability&lt;/strong&gt;: ISPC provides a unified programming model that can target multiple architectures (x86, ARM) and instruction sets, offering better portability than CUDA.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img alt="ISPC SIMD Processing Model" src="http://genn-team.github.io/images/SIMD2.png"&gt;
&lt;em&gt;Figure 2: ISPC's Single Program, Multiple Data (SPMD) execution model enables efficient utilization of CPU SIMD units by processing multiple data elements in parallel.&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;Problem Statement&lt;/h4&gt;
&lt;p&gt;The primary goal of the project was to develop a backend that could:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use SIMD parallelization for neural network computations&lt;/li&gt;
&lt;li&gt;Provide CPU-based performance better than the single-threaded CPU backend&lt;/li&gt;
&lt;li&gt;Maintain compatibility with existing GeNN model definitions&lt;/li&gt;
&lt;li&gt;Support cross-platform deployment (Windows, Linux, macOS)&lt;/li&gt;
&lt;li&gt;Handle complex memory access patterns required for ISPC vectorization&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;Project Aim &amp;amp; Objectives&lt;/h3&gt;
&lt;h4&gt;Primary Aim&lt;/h4&gt;
&lt;p&gt;Develop a fully functional ISPC backend for GeNN that enables SIMD-accelerated neural network simulations on CPU hardware.&lt;/p&gt;
&lt;h4&gt;Specific Objectives&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Backend Architecture Implementation&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Integrate ISPC code generation into GeNN's existing backend framework&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Implement kernel generation for neuron and synapse updates&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Memory Management Optimization&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Develop efficient memory access patterns for SIMD operations&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Handle memory alignment requirements for vectorized operations&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Feature Compatibility&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Ensure compatibility with existing GeNN neuron and synapse models&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Support custom update operations and user-defined functions&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Performance Evaluation&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Benchmark ISPC backend against CPU and GPU implementations&lt;/li&gt;
&lt;li&gt;Analyze performance across different model sizes and connectivity patterns&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Evaluate cross-platform performance&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Integration and Testing&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;Integrate with GeNN's build system&lt;/li&gt;
&lt;li&gt;Ensure compatibility with PyGeNN Python interface&lt;/li&gt;
&lt;li&gt;Ensure correctness through existing test suite&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;Methodology&lt;/h3&gt;
&lt;h4&gt;Tools and Frameworks&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Development Environment:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Intel SPMD Program Compiler (ISPC) v1.27.0&lt;/li&gt;
&lt;li&gt;Visual Studio 2022 (Windows development)&lt;/li&gt;
&lt;li&gt;Ubuntu on WSL2 (cross-platform testing)&lt;/li&gt;
&lt;li&gt;Git version control with GitHub integration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Programming Languages:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C++ for backend implementation&lt;/li&gt;
&lt;li&gt;ISPC for kernel development&lt;/li&gt;
&lt;li&gt;Python for testing and benchmarking (PyGeNN)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Testing and Benchmarking:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Custom benchmarking scripts for performance evaluation&lt;/li&gt;
&lt;li&gt;GeNN's existing test suite for feature tests&lt;/li&gt;
&lt;li&gt;Profiling tools for phase-wise performance analysis &lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Implementation Approach&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. Code Generation Pipeline:&lt;/strong&gt;
The ISPC backend follows GeNN's established code generation pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Model analysis and kernel identification&lt;/li&gt;
&lt;li&gt;ISPC kernel code generation with SIMD based on user's target ISA&lt;/li&gt;
&lt;li&gt;Host code generation for the kernel&lt;/li&gt;
&lt;li&gt;Optimized memory management &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Kernel Development Strategy:&lt;/strong&gt;
- Adapt neuron and synapse update models from the Single Threaded CPU backend&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Replace bitwise operations with atomic operations to accomodate for multiple lanes&lt;/li&gt;
&lt;li&gt;Vectorize user-defined models through custom update kernels&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. Memory Layout Optimization:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Aligned memory allocation for vectorized access&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Testing Methodology:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Performance benchmarking across multiple platforms&lt;/li&gt;
&lt;li&gt;Correctness validation against reference implementations&lt;/li&gt;
&lt;li&gt;Using pre-existing feature tests for the backend&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;Work Done&lt;/h3&gt;
&lt;h4&gt;Phase 1: Foundation and Architecture Setup (Weeks 1-2)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Backend Infrastructure Development:&lt;/strong&gt;
The initial phase focused on establishing the foundational architecture for the ISPC backend within GeNN's existing framework. This involved creating key files as well as the Array and Preferences class.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Skeleton File Structure:&lt;/strong&gt; Established the complete directory structure for the ISPC backend, including Makefile configuration and visual studio project file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Array Class Implementation:&lt;/strong&gt; Developed specialized array handling classes to manage SIMD-aligned memory layouts, ensuring optimal performance for vectorized operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backend Class Foundation:&lt;/strong&gt; Created the &lt;code&gt;Backend&lt;/code&gt; class in the ISPC namespace inheriting from &lt;code&gt;BackendBase&lt;/code&gt;, implementing the essential virtual function signatures required by GeNN's code generation pipeline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Key Technical Contributions:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Created the foundational code generation framework&lt;/li&gt;
&lt;li&gt;Established memory alignment and preferences requirements for SIMD operations&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Phase 2: Core Kernel Implementation (Weeks 3-8)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Neuron and Synapse Update Kernels:&lt;/strong&gt;
This phase involved the systematic adaptation of existing single-threaded CPU kernels to leverage ISPC's SIMD capabilities. The approach focused on identifying parallelizable operations and implementing ataomic operations for thread safety.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Neuron Update Vectorization:&lt;/strong&gt; Transformed scalar neuron state update loops into SIMD-optimized ISPC kernels using &lt;code&gt;foreach&lt;/code&gt; constructs to process multiple neurons simultaneously&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Synaptic Processing Optimization:&lt;/strong&gt; Adapted synaptic weight updates and spike propagation algorithms to utilize vector operations, significantly improving throughput for dense connectivity patterns&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dependency Method Implementation:&lt;/strong&gt; Systematically vectorized all supporting functions including preSynatpicUpdates, postSynapticUpdates, genPrevEventTimeUpdate etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Technical Implementation Strategy:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Refactored existing single-threaded CPU backend as the foundation, strategically adding &lt;code&gt;foreach&lt;/code&gt; parallelization constructs&lt;/li&gt;
&lt;li&gt;Implemented efficient atomic operations to replace the bit wise operations for thread safety.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Phase 3: Backend Integration and Setup (Weeks 8-10)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;PyGeNN Integration and System Configuration:&lt;/strong&gt;
The integration phase focused on making the ISPC backend accessible through GeNN's Python interface and ensuring usability on different platforms.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python Binding Extension:&lt;/strong&gt; Extended PyGeNN to recognize and utilize the ISPC compiler and backend, using pre-existing backend selection mechanisms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-Platform Setup:&lt;/strong&gt; Configured build systems for Windows and Linux environments, addressing platform-specific ISPC compiler requirements and library linking&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Runtime Configuration:&lt;/strong&gt; Implemented SIMD width allocation based on user's Instruction Set Architecture &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;System Integration Achievements:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Integrated ISPC backend with existing GeNN model setup and Python&lt;/li&gt;
&lt;li&gt;Target based SIMD instruction set (SSE, AVX, AVX2, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Phase 4: Advanced Features and Performance Optimization (Weeks 11-12)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Custom Update Operations and Benchmarking:&lt;/strong&gt;
The final phase focused on extending functionality to support user-defined operations and conducting comprehensive performance evaluation across multiple scenarios.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Custom Update Kernel Generation:&lt;/strong&gt; Adapted the existing custom update framework for ISPC by applying &lt;code&gt;foreach&lt;/code&gt; parallelization to user-defined mathematical operations and reduction algorithms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Comprehensive Benchmarking Suite:&lt;/strong&gt; Extensive performance tests were conducted for multiple neuron counts (4000, 8000, 16000, 32000, 64000) for all backends on both Windows native and WSL environments&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance Data Collection:&lt;/strong&gt; Systematically gathered per-phase timing data and memory usage statistics to compare the performance achieved through SIMD vectorization with other backends&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Benchmarking Methodology:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Utilized existing GeNN usage example code as the foundation for performance tests&lt;/li&gt;
&lt;li&gt;Conducted comparative analysis against single-threaded CPU and cuda backends&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Key Technical Contributions&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;SIMD Kernel Adaptation Strategy:&lt;/strong&gt;
The core technical achievement involved the systematic refactoring of existing single-threaded algorithms into SIMD-optimized implementations. This was accomplished through strategic application of ISPC's &lt;code&gt;foreach&lt;/code&gt; construct, which enabled automatic vectorization while preserving functional correctness.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Backend Architecture Implementation:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// Core backend methods successfully implemented&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;genNeuronUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CodeStream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ModelSpec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NeuronGroupInternal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ng&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Substitutions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;popSubs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;genSynapseUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CodeStream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ModelSpec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SynapseGroupInternal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;sg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                      &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Substitutions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;popSubs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;genCustomUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CodeStream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ModelSpec&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CustomUpdateInternal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Substitutions&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;popSubs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Vectorization Methodology:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Foreach Parallelization:&lt;/strong&gt; Systematically identified scalar loops in existing CPU backend and applied &lt;code&gt;foreach&lt;/code&gt; constructs to enable SIMD processing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Layout Optimization:&lt;/strong&gt; Implemented Array class to ensure optimal memory access patterns for vectorized operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Algorithm Preservation:&lt;/strong&gt; Maintained exact functional behavior of original implementations while achieving significant performance improvements through parallelization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Integration Achievements:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Successful integration with GeNN's existing code generation pipeline&lt;/li&gt;
&lt;li&gt;Full compatibility with PyGeNN Python interface&lt;/li&gt;
&lt;li&gt;Capable of cross-platform deployment across Windows and Linux environments&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;Results &amp;amp; Analysis&lt;/h3&gt;
&lt;h4&gt;Performance Benchmarking&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Test Configuration:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Hardware: Intel Core i7-12700K (AVX2 support, 256bit-8 lanes)&lt;/li&gt;
&lt;li&gt;Operating Systems: Windows 11, Ubuntu 22.04 (WSL2)&lt;/li&gt;
&lt;li&gt;Comparison Backends:  ISPC, Single-thread CPU, CUDA (RTX 3050)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Benchmark Models: Vogels-Abbott Network&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Dense Network (4000, 8000, 16000 and 32000 neurons)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sparse Network (4000, 8000, 16000, 32000 and 64000 neurons)&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Detailed Performance Data:&lt;/strong&gt;
Complete benchmarking results, including raw timing data, memory usage statistics, and cross-platform comparisons are available in the &lt;a href="https://docs.google.com/spreadsheets/d/1gJQmLC9h9WJT5Wl508GS2M-N4OALJ9hQ410A1KKgGMU/edit?usp=sharing"&gt;Performance Analysis Spreadsheet&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Performance Results&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Comprehensive Benchmarking Across Multiple Scales:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sparse Networks:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single-thread CPU: 1.0x baseline&lt;/li&gt;
&lt;li&gt;ISPC on i7 12700H (AVX2): 1.4x speedup&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Dense Networks:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Single-thread CPU: 1.0x baseline&lt;/li&gt;
&lt;li&gt;ISPC on Intel i5 (AVX2): 3.05x speedup &lt;/li&gt;
&lt;li&gt;ISPC on i7 12700H (AVX2): 3.1x speedup&lt;/li&gt;
&lt;li&gt;ISPC on Xeon Gold 6134 (AVX512): 9.49x speedup &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Cross-Platform Performance Comparison:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Windows vs WSL2 Single-threaded:&lt;/strong&gt; WSL2 demonstrated inferior single-threaded performance and superior ISPC performance&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ISPC Performance:&lt;/strong&gt; WSL2 ISPC implementation achieved 50-60% execution time of Windows ISPC for dense networks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sparse Network Optimization:&lt;/strong&gt; WSL2 ISPC sparse networks executed in approximately 0.35x the time of Windows implementation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Bandwidth Utilization:&lt;/strong&gt; Achieved 60-75% of theoretical peak across all test configurations. This was noted through the CPU utilizataion graph&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Key Observations&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Unexpected WSL2 Performance Advantage&lt;/strong&gt;: Contrary to expectations, WSL2 demonstrated superior performance for ISPC implementations, with ISPC for dense tests achieving 40-50% better execution times than Windows native&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hardware-Dependent Scaling&lt;/strong&gt;: Significant performance variation was observed across different CPU architectures, with Xeon Gold 6134 achieving 9.49x speedup compared to 3.05x on Intel i5. This is due to the advanced ISA on the Xeon Gold 6134 allowing 16 parallel lanes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SIMD Vectorization Efficiency&lt;/strong&gt;: Achieved 60% of theoretical SIMD peak performance across all tested configurations on AVX-512 and ~40% on AVX-256 ISA &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory Subsystem Optimization&lt;/strong&gt;: WSL2's memory management appears better optimized for SIMD workloads, particularly benefiting sparse network computations (~0.35x of Windows ISPC simulation time)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-Platform Portability&lt;/strong&gt;: Successful deployment across Windows and Linux environments with platform-specific performance characteristics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vectorization Success&lt;/strong&gt;: Successful adaptation of existing scalar algorithms to SIMD paradigm without algorithmic modifications, maintaining numerical accuracy across platforms&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;Challenges Faced&lt;/h3&gt;
&lt;h4&gt;Technical Challenges&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;1. Understanding GeNN's Complex Architecture:&lt;/strong&gt;
GeNN is a well-structured library with intricate code generation pipelines and backend methods. Before any implementation could begin, I invested time in studying the existing backends and their design patterns. With guidance from my mentor, I developed a clear understanding of how different components interact, which formed the foundation for all subsequent development work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Build System Integration:&lt;/strong&gt;
Integrating ISPC compiler and build dependencies into GeNN's existing CMake-based build system was tricky. My mentor's assistance in configuring library linking and cross-platform compilation was particularly helpful in building the ISPC backend.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Dual Code Generation Strategy:&lt;/strong&gt;
ISPC keywords are not recognised in a standard C++ file and therefore the backend required managing two separate code streams - C++ host code (for .cc files) and ISPC kernel code (for .ispc files) with their respective dependencies. Initialization was managed in the C++ file while parallel computations were managed in the ISPC ones. This helped in achieving a clean code organization and optimal performance.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Future Improvements&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Batch Size Optimization:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement support for batch sizes greater than 1 to process multiple simulation steps simultaneously&lt;/li&gt;
&lt;li&gt;Leverage SIMD width more effectively by processing multiple timesteps in parallel&lt;/li&gt;
&lt;li&gt;Optimize memory access patterns for batched operations to improve cache utilization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Automatic Instruction Set Detection:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement runtime detection of optimal SIMD instruction set architecture (SSE, AVX, AVX2, AVX-512)&lt;/li&gt;
&lt;li&gt;Automatically select the best performing instruction set based on available hardware capabilities&lt;/li&gt;
&lt;li&gt;Provide fallback mechanisms for older processors while maximizing performance on newer architectures&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;3. Native ISPC Implementation of Core Functions:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Implement Random Number Generation (RNG) and other utility methods directly in ISPC&lt;/li&gt;
&lt;li&gt;Reduce time spent on C++ initialization by moving more functionality to ISPC kernels&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The development of an ISPC backend for GeNN successfully addresses the performance gap between single-threaded CPU and GPU implementations. The project achieved its primary objectives by delivering a fully functional backend that provides significant performance improvements while maintaining compatibility with existing GeNN models.&lt;/p&gt;
&lt;h4&gt;Key Achievements&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Performance Impact&lt;/strong&gt;: Delivered significant speedup over single-threaded CPU implementations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Accessibility&lt;/strong&gt;: Enabled high-performance neural simulations on standard CPU hardware&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Portability&lt;/strong&gt;: Provided cross-platform compatibility across Windows, Linux, and macOS&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration&lt;/strong&gt;: Seamlessly integrated with existing GeNN ecosystem and PyGeNN interface&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;Community Impact&lt;/h4&gt;
&lt;p&gt;The ISPC backend significantly lowers the barrier to entry for high-performance neural network simulations. Researchers without access to specialized GPU hardware can now achieve considerable performance jumps for medium-scale simulations. This democratization of computational neuroscience tools aligns with GeNN's mission to make neural network simulation accessible to a broader research community.&lt;/p&gt;
&lt;p&gt;The successful completion of this project establishes a foundation for future developments in CPU-based neural network acceleration and demonstrates the viability of SIMD programming for computational neuroscience applications.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Acknowledgments&lt;/h3&gt;
&lt;p&gt;I would like to express my sincere gratitude to my mentors, &lt;strong&gt;Dr. Jamie Knight&lt;/strong&gt; and &lt;strong&gt;Dr. Thomas Nowotny&lt;/strong&gt;, whose invaluable guidance, expertise, and continuous support made this project possible. Their deep knowledge of GeNN's architecture and SIMD programming principles was instrumental in navigating the complexities of backend development and achieving the project's objectives.&lt;/p&gt;
&lt;p&gt;Special thanks to Dr. Knight for his assistance with the build system integration and initialization architecture. His mentorship not only helped me complete this project successfully but also significantly aided my understanding of high-performance computing and computational neuroscience.&lt;/p&gt;
&lt;p&gt;I am also grateful to the INCF organization and the GeNN development team for providing this opportunity through Google Summer of Code 2025, and for their commitment to advancing open-source tools in computational neuroscience.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Intel Corporation. (2023). &lt;em&gt;Intel SPMD Program Compiler User's Guide&lt;/em&gt;. &lt;a href="https://ispc.github.io/"&gt;Available online&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Intel Corporation. (2013). &lt;em&gt;SIMD Made Easy with Intel ISPC&lt;/em&gt;. &lt;a href="https://www.intel.com/content/dam/develop/external/us/en/documents/simd-made-easy-with-intel-ispc.pdf"&gt;Available online&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Pharr, M., &amp;amp; Mark, W. R. (2012). ispc: A SPMD compiler for high-performance CPU programming. &lt;em&gt;Proceedings of Innovative Parallel Computing (InPar)&lt;/em&gt;. &lt;a href="https://ieeexplore.ieee.org/document/6339601"&gt;Available online&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Yavuz, E., Turner, J., &amp;amp; Nowotny, T. (2016). GeNN: a code generation framework for accelerated brain simulations. &lt;em&gt;Scientific Reports&lt;/em&gt;, 6, 18854. &lt;a href="https://www.nature.com/articles/srep18854"&gt;Available online&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Knight, J. C., Komissarov, A., &amp;amp; Nowotny, T. (2021). PyGeNN: A Python Library for GPU-Enhanced Neural Networks. &lt;em&gt;Frontiers in Neuroinformatics&lt;/em&gt;, 15, 659005. &lt;a href="https://www.frontiersin.org/journals/neuroinformatics/articles/10.3389/fninf.2021.659005/full"&gt;Available online&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Vogels, T. P., &amp;amp; Abbott, L. F. (2005). Signal propagation and logic gating in networks of integrate-and-fire neurons. &lt;em&gt;Journal of Neuroscience&lt;/em&gt;, 25(46), 10786-10795. &lt;a href="https://www.jneurosci.org/content/25/46/10786"&gt;Available online&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Hennessy, J. L., &amp;amp; Patterson, D. A. (2019). &lt;em&gt;Computer architecture: a quantitative approach&lt;/em&gt;. Morgan Kaufmann.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;This report was prepared as part of the Google Summer of Code 2025 program.&lt;/em&gt;&lt;/p&gt;</description><category>GSOC</category><guid>http://genn-team.github.io/posts/developing-an-ispc-backend-for-genn-bridging-gpu-and-cpu-performance-for-neural-network-simulations.html</guid><pubDate>Wed, 17 Sep 2025 12:45:49 GMT</pubDate></item><item><title>Software Developer Blog: Running away</title><link>http://genn-team.github.io/posts/running-away.html</link><dc:creator>Jamie</dc:creator><description>&lt;p&gt;After spending a long time updating GeNN's code generator to generate more efficient CUDA kernels which have the side benefit of compiling much more quickly, there remained something of a dirty secret.
The &lt;code&gt;runner.cc&lt;/code&gt; file which contains the helper functions generated by GeNN for allocating memory and copying variables between GPU and CPU could still easily grow to the point that compilation would take an extremely long time and consume all available memory.
For our &lt;a href="https://github.com/neworderofjamie/multi-area-model/"&gt;multi-area model implementation&lt;/a&gt;, I added various options which turn off the generation of empty functions and, as everything in this model was generated on the GPU anyway, I also turned off the generation of host copies of almost all variables.
This resulted in a paltry 40 mbyte &lt;code&gt;runner.cc&lt;/code&gt; which compiled in a couple of minutes which, for a model this size, is just about acceptable.
However, as users have started making bigger models and not always wanting to generate everything on the GPU, this issue has kept reappearing.&lt;/p&gt;
&lt;h2&gt;Jinjaly investigating&lt;/h2&gt;
&lt;p&gt;To investigate this in a slightly simpler way than just building larger and larger GeNN models until things break, I used &lt;a href="https://jinja.palletsprojects.com"&gt;Jinja&lt;/a&gt; to build a template that could generate fake &lt;code&gt;runner.cc&lt;/code&gt; files containing varying number of arrays, representing the state variables in a real model. 
The heart of this template looked something like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// Push and pull functions&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;arrays&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="n"&gt;ToDevice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;CHECK_CUDA_ERRORS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cudaMemcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                                 &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                                 &lt;/span&gt;&lt;span class="n"&gt;cudaMemcpyHostToDevice&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pull&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="n"&gt;FromDevice&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;CHECK_CUDA_ERRORS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cudaMemcpy&lt;/span&gt;&lt;span class="p"&gt;({{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;d_&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                                 &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                                 &lt;/span&gt;&lt;span class="n"&gt;cudaMemcpyDeviceToHost&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;endfor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;allocateMem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;CHECK_CUDA_ERRORS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cudaSetDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;arrays&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;CHECK_CUDA_ERRORS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cudaHostAlloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                                    &lt;/span&gt;&lt;span class="n"&gt;cudaHostAllocPortable&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;CHECK_CUDA_ERRORS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cudaMalloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;d_&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                                 &lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;endfor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;this template (saved in &lt;code&gt;runner.cc.template&lt;/code&gt;) could then be used to generate C++ and print it to stdout like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;jinja2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Template&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"runner.cc.template"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"r"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;arrays&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"array_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; 
          &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_arrays&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;


&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;template&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arrays&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;arrays&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On Linux, the C++ could then be built using the same command line used by GeNN itself (some options omitted for brevity) and timed using the &lt;code&gt;/usr/bin/time&lt;/code&gt; (I recently discovered that &lt;code&gt;command time&lt;/code&gt; can be used as an alternative way of disambiguating this from the bash builtin &lt;code&gt;time&lt;/code&gt;):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;/usr/bin/time&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;nvcc&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;-x&lt;span class="w"&gt; &lt;/span&gt;cu&lt;span class="w"&gt; &lt;/span&gt;-arch&lt;span class="w"&gt; &lt;/span&gt;sm_86&lt;span class="w"&gt; &lt;/span&gt;-std&lt;span class="o"&gt;=&lt;/span&gt;c++11&lt;span class="w"&gt; &lt;/span&gt;test.cc
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Initial experiments showed that while both the wall clock time and maximum resident set size (roughly analagous to peak physical memory usage) grow approximately linearly (much to my relief after reading way too many &lt;a href="https://randomascii.wordpress.com/category/quadratic/"&gt;horror stories&lt;/a&gt; on Bruce Dawson's &lt;a href="https://randomascii.wordpress.com"&gt;excellent blog&lt;/a&gt;) with the number of arrays, it still grew extremely rapidly:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Relationship between runner size; and compile time and memory usage" src="http://genn-team.github.io/images/blog_running_away/fig_1_linearity.png"&gt;&lt;/p&gt;
&lt;p&gt;Therefore, a model with 10000 arrays will take over 4 minutes and around 8 gbyte of memory to compile — neither of which are really acceptable.
To put this in perspective, if you split a model up into about 100 populations and connect most of the permutations together (this is an all-too-reasonable assumption in many areas of the mammalian brain), you could easily reach this many variables.&lt;/p&gt;
&lt;p&gt;So....what is NVCC doing with all this time and memory?
&lt;code&gt;runner.cc&lt;/code&gt; only contains host code (NVCC is just used to ensure the same compiler/options across execution units and to deal with setting up the CUDA linker/include paths) but, when you pass a 5 mbyte &lt;code&gt;runner.cc&lt;/code&gt; file to NVCC, the file that is passed on to the host compiler (GCC) has grown to 15 mbyte!
However, this turned out to be simply because NVCC is in charge of running the preprocessor so that 10 mbyte is 'just' the result of expanding macros and including C++ standard library header files!&lt;/p&gt;
&lt;h2&gt;Profiling&lt;/h2&gt;
&lt;p&gt;Around this point, I remembered reading a blog post about &lt;a href="https://aras-p.info/blog/2019/01/12/Investigating-compile-times-and-Clang-ftime-report/"&gt;profiling compiler times&lt;/a&gt; on yet another &lt;a href="https://aras-p.info/blog/"&gt;excellent blog&lt;/a&gt; and turned on the &lt;code&gt;-ftime-report&lt;/code&gt; GCC option.
As the blog promised, this generates a gargantuan report which starts by splitting the compilation time of this 10000 array model into 'phases':&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;phase setup             :  ...   0.00 ( 0%) wall    1384 kB ( 0%)
phase parsing           :  ...  20.81 ( 9%) wall 1794944 kB (25%)
phase lang. deferred    :  ...   0.02 ( 0%) wall    2426 kB ( 0%)
phase opt and generate  :  ... 214.14 (91%) wall 5412439 kB (75%)
phase finalize          :  ...   0.54 ( 0%) wall       0 kB ( 0%)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Somewhat surprisingly (as, after all, we're throwing a massive source file at GCC), the vast majority of time is spent in "opt and generate" (code-generation and optimisation) rather than in parsing.
Looking a little further down, where the report contains a seeminly unsorted list of processes within phases, the only other 'hot' line is:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;expand vars             :  ... 101.93 (43%) wall   50597 kB ( 1%)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;but, as a non-GCC developer, this doesn't help me a great deal....back to pursuing random hunches!&lt;/p&gt;
&lt;h2&gt;Smoking gun&lt;/h2&gt;
&lt;p&gt;As there's no virtual functions in this code, my C++ prejudices suggest that only exceptions could &lt;em&gt;possibly&lt;/em&gt; be to blame and, as each of those &lt;code&gt;CHECK_CUDA_ERRORS&lt;/code&gt; macros hides a &lt;code&gt;throw std::runtime_error&lt;/code&gt;, maybe that's not unreasonable.
Generating all that zero-cost abstraction must involve &lt;em&gt;expanding&lt;/em&gt; a lot of &lt;em&gt;variables&lt;/em&gt;....right!?
How about if we replace our current implemementation of &lt;code&gt;CHECK_CUDA_ERRORS&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define CHECK_CUDA_ERRORS(call) {\&lt;/span&gt;
&lt;span class="cp"&gt;    cudaError_t error = call;\&lt;/span&gt;
&lt;span class="cp"&gt;    if(error != cudaSuccess) {\&lt;/span&gt;
&lt;span class="cp"&gt;        throw std::runtime_error(__FILE__": " \&lt;/span&gt;
&lt;span class="cp"&gt;                                 + std::to_string(__LINE__) \&lt;/span&gt;
&lt;span class="cp"&gt;                                 + ": cuda error " \&lt;/span&gt;
&lt;span class="cp"&gt;                                 + std::to_string(error) \&lt;/span&gt;
&lt;span class="cp"&gt;                                 + ": " + cudaGetErrorString(error));\&lt;/span&gt;
&lt;span class="cp"&gt;    }\&lt;/span&gt;
&lt;span class="cp"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;with:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define CHECK_CUDA_ERRORS(call) {\&lt;/span&gt;
&lt;span class="cp"&gt;    cudaError_t error = call;\&lt;/span&gt;
&lt;span class="cp"&gt;    assert(error == cudaSuccess); \&lt;/span&gt;
&lt;span class="cp"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;or even:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define CHECK_CUDA_ERRORS(call) {\&lt;/span&gt;
&lt;span class="cp"&gt;    cudaError_t error = call;\&lt;/span&gt;
&lt;span class="cp"&gt;    if(error != cudaSuccess) {\&lt;/span&gt;
&lt;span class="cp"&gt;        std::abort();\&lt;/span&gt;
&lt;span class="cp"&gt;    }\&lt;/span&gt;
&lt;span class="cp"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Some template-meddling and sweeping later we can produce:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Relationship between number of arrays and error handling approach; and compile time and memory usage" src="http://genn-team.github.io/images/blog_running_away/fig_2_alternate_error.png"&gt;&lt;/p&gt;
&lt;p&gt;Seems like this actually works! Our 10000 array model now only takes 30 seconds and less than 1 gbyte of memory to compile which is much more reasonable!&lt;/p&gt;
&lt;p&gt;However, are the exceptions &lt;em&gt;really&lt;/em&gt; to blame? 
Compile times seem much better when using the single-threaded CPU backend and that allocates memory with &lt;code&gt;new[]&lt;/code&gt; (obviously, modern C++ rules don't apply in generated code...) which throws &lt;code&gt;std::bad_alloc&lt;/code&gt; to signal failure.
Admittedly, because there's no need to copy data when everthing's on the CPU, this backend generates empty 'push' and 'pull' functions so there's less code to compile overall but, if generating exception handling code was the problem, you would expect issues here too.
Maybe expanding all that message-generating code is the real issue...
How about we hack the following additional variants into the template:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define CHECK_CUDA_ERRORS(call) {\&lt;/span&gt;
&lt;span class="cp"&gt;    cudaError_t error = call;\&lt;/span&gt;
&lt;span class="cp"&gt;    if(error != cudaSuccess) {\&lt;/span&gt;
&lt;span class="cp"&gt;        throw std::runtime_error();\&lt;/span&gt;
&lt;span class="cp"&gt;    }\&lt;/span&gt;
&lt;span class="cp"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define CHECK_CUDA_ERRORS(call) {\&lt;/span&gt;
&lt;span class="cp"&gt;    cudaError_t error = call;\&lt;/span&gt;
&lt;span class="cp"&gt;    if(error != cudaSuccess) {\&lt;/span&gt;
&lt;span class="cp"&gt;        std::cerr &amp;lt;&amp;lt; __FILE__ &amp;lt;&amp;lt; ": " &amp;lt;&amp;lt; __LINE__;\&lt;/span&gt;
&lt;span class="cp"&gt;        std::cerr &amp;lt;&amp;lt; ": cuda error " &amp;lt;&amp;lt; error &amp;lt;&amp;lt; ": ";\&lt;/span&gt;
&lt;span class="cp"&gt;        std::cerr &amp;lt;&amp;lt; cudaGetErrorString(error) &amp;lt;&amp;lt; std::endl;\&lt;/span&gt;
&lt;span class="cp"&gt;        std::abort();\&lt;/span&gt;
&lt;span class="cp"&gt;    }\&lt;/span&gt;
&lt;span class="cp"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and sweep:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Relationship between number of arrays and error handling approach; and compile time and memory usage" src="http://genn-team.github.io/images/blog_running_away/fig_3_more_alternate_error.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::abort&lt;/code&gt; is definitely easier on the compiler than throwing exceptions but, compiling the message generation code also seems to make a large difference.&lt;/p&gt;
&lt;h2&gt;MSVC&lt;/h2&gt;
&lt;p&gt;The other compiler I often use with GeNN is Microsoft Visual C++.
I can't quite face repeating this whole process again but, initial tests suggest that this optimisation is even more valuable here.
Using the simplest &lt;code&gt;std::abort&lt;/code&gt; raising &lt;code&gt;CHECK_CUDA_ERRORS&lt;/code&gt; macro, the 10000 array model can be compiled in around 19 seconds whereas, using the original exception-throwing macro...I have given up waiting after around 1 hour!&lt;/p&gt;
&lt;h2&gt;Implementing a workaround&lt;/h2&gt;
&lt;p&gt;GeNN is approaching the end of the 4.X release cycle so, for now, I have added a simple but slightly hacky workaround for these issues by adding a &lt;code&gt;generateSimpleErrorHandling&lt;/code&gt; flag to GeNN's CUDA backend to switch from generating code with the previous full-fat &lt;code&gt;CHECK_CUDA_ERRORS&lt;/code&gt; macro to the simplest version which simply calls &lt;code&gt;std::abort&lt;/code&gt; without generating a message. This can be turned on from C++ like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;modelDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NNmodel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;GENN_PREFERENCES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;generateSimpleErrorHandling&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;or from Python like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GeNNModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"float"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"my_model"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;generateSimpleErrorHandling&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Real models&lt;/h2&gt;
&lt;p&gt;The largest model we currently have to play with with is the &lt;a href="https://github.com/neworderofjamie/multi-area-model/"&gt;multi-area cortical model&lt;/a&gt;.
Although it has 64516 synapse groups, due to its use of procedural connectivity (where all synaptic connectivity, weights and delays are generated on the fly), it doesn't actually have any per-synapse group variables with push and pull functions.
Nonetheless, using the new simple error handling reduces the compilation time of the &lt;code&gt;runner.cc&lt;/code&gt; from 155 to 129 seconds.
Finally, although it is not possible to &lt;em&gt;run&lt;/em&gt; the model in this way as no single GPU has enough memory, we can generate a &lt;code&gt;runner.cc&lt;/code&gt; from this model with standard, in-memory sparse connectivity and push and pull functions for each variable.
This results in a nightmarish, 114 mbyte &lt;code&gt;runner.cc&lt;/code&gt; which, using the original &lt;code&gt;CHECK_CUDA_ERRORS&lt;/code&gt; macro, would definitely be impossible to compile on any reasonable machine.
However, using the new simplified macro, the runner can be compiled in just over 20 minutes and requires just over 14 gbyte of memory — still pretty unusable but definitely progress!&lt;/p&gt;
&lt;h2&gt;Long-term solutions&lt;/h2&gt;
&lt;p&gt;The majority of the time, the errors which the &lt;code&gt;CHECK_CUDA_ERRORS&lt;/code&gt; macro is aiming to catch are out of memory errors in the &lt;code&gt;allocateMem&lt;/code&gt; function and errors that occured during (asynchronous) kernel launches that are only caught at the next push or pull call (which are typically the main synchronisation points) so perhaps, in future, we could adopt a more targetted error-handling approach which provides a balance between sufficient debugging information and compilation time.&lt;/p&gt;
&lt;p&gt;However, while the error handling changes discussed here allow the current approach to generating &lt;code&gt;runner.cc&lt;/code&gt; files to scale a bit further, the code we are generating is still pretty pathological, least of all because &lt;a href="https://github.com/genn-team/genn/issues/408"&gt;the Windows PE executable format has a limit of 65535 symbol limit&lt;/a&gt; which you can hit quite easily with a large model.
Early this year, I made an attempt at re-writing the code generator to apply the same merging strategy GeNN uses elsewhere to &lt;code&gt;runner.cc&lt;/code&gt;. 
This means that all the variables associated with neuron and synapse populations with the same types of state variable can be allocated using one piece of shared generated code.
While this works, it adds yet more complexity to GeNN and fundamentally breaks the 'classic' way of using GeNN from C++, where you link some C++ simulation code against your generated code and can access state variables directly by name.
However, based on this investigation, maybe that project needs resurrecting!&lt;/p&gt;
&lt;p&gt;All the code I've developed to explore this problem is available from &lt;a href="https://github.com/neworderofjamie/nvcc_breaker"&gt;my Github&lt;/a&gt;.&lt;/p&gt;</description><guid>http://genn-team.github.io/posts/running-away.html</guid><pubDate>Tue, 01 Nov 2022 13:35:07 GMT</pubDate></item><item><title>Software Developer Blog: How to do convolutions with doubly blocked Toeplitz matrices</title><link>http://genn-team.github.io/posts/sw_blog_toeplitz.html</link><dc:creator>Thomas Nowotny</dc:creator><description>&lt;h2&gt;How to do convolutions with doubly blocked Toeplitz matrices&lt;/h2&gt;
&lt;p&gt;A few weeks ago, Jamie (@neworderofjamie) asked me on the chat whether I knew what doubly blocked Toeplitz matrices are and how they implement convolutions. I had no clue. Since then we have implemented convolutions using doubly blocked Toeplitz matrices in GeNN and found them to be extremely useful and efficient.
1
In this software blog I will give a brief overview on the why and how convolutions relate to doubly blocked Toeplitz matrices. My blog is based on Ali Salehi's tutorial &lt;a href="https://raw.githubusercontent.com/alisaaalehi/convolution_as_multiplication/master/ConvAsMulExplained.pdf"&gt;Convolution as Matrix Multiplication&lt;/a&gt;  but updated to use machine-learning rather than signal-processing conventions and I am trying to avoid using too many unusual ways of re-arranging rows and columns.&lt;/p&gt;
&lt;h3&gt;The why&lt;/h3&gt;
&lt;p&gt;Let us consider the convolution of a \(2\times 2\) kernel with a \(3\times 3\) layer. We denote the kernel as
\[
K= \left(\matrix{
k_{11} &amp;amp; k_{12} \cr
k_{21} &amp;amp; k_{22}}\right)
\]
and the layer as
\[
I= \left(\matrix{
i_{11} &amp;amp; i_{12} &amp;amp; i_{13} \cr
i_{21} &amp;amp; i_{22} &amp;amp; i_{23} \cr
i_{31} &amp;amp; i_{32} &amp;amp; i_{33}
} \right).
\]
Then the convolution in the machine learning use of the term is calculating the cross-correlation of the kernel "moving across" the layer as illustrated below. The layer \(I\) is in blue, the kernel \(K\) in grey and the result \(R\) in green.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: center;"&gt;&lt;sup id="fnref:1"&gt;&lt;a class="footnote-ref" href="http://genn-team.github.io/posts/sw_blog_toeplitz.html#fn:1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;img alt="Illustration of convolution step" src="http://genn-team.github.io/images/blog_00.png"&gt;&lt;/th&gt;
&lt;th style="text-align: center;"&gt;&lt;img alt="Illustration of convolution step" src="http://genn-team.github.io/images/blog_01.png"&gt;&lt;/th&gt;
&lt;th style="text-align: center;"&gt;&lt;img alt="Illustration of convolution step" src="http://genn-team.github.io/images/blog_02.png"&gt;&lt;/th&gt;
&lt;th style="text-align: center;"&gt;&lt;img alt="Illustration of convolution step" src="http://genn-team.github.io/images/blog_03.png"&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;\(r_{11}\)&lt;/td&gt;
&lt;td style="text-align: center;"&gt;\(r_{12}\)&lt;/td&gt;
&lt;td style="text-align: center;"&gt;\(r_{13}\)&lt;/td&gt;
&lt;td style="text-align: center;"&gt;\(3_{14}\)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For the first non-zero entry at \((1,1)\) of the result matrix \(R\), we therefore have \(r_{11} = k_{22} i_{11}\).
Then the kernel moves one over and \(r_{12} = k_{21}i_{11} + k_{22} i_{12}\). Then, \(r_{13} = k_{21}i_{12} + k_{22} i_{13}\) and \(r_{14} = k_{21}i_{13} \).&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: center;"&gt;&lt;img alt="Illustration of convolution step" src="http://genn-team.github.io/images/blog_04.png"&gt;&lt;/th&gt;
&lt;th style="text-align: center;"&gt;&lt;img alt="Illustration of convolution step" src="http://genn-team.github.io/images/blog_05.png"&gt;&lt;/th&gt;
&lt;th style="text-align: center;"&gt;&lt;img alt="Illustration of convolution step" src="http://genn-team.github.io/images/blog_06.png"&gt;&lt;/th&gt;
&lt;th style="text-align: center;"&gt;&lt;img alt="Illustration of convolution step" src="http://genn-team.github.io/images/blog_07.png"&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: center;"&gt;\(r_{21}\)&lt;/td&gt;
&lt;td style="text-align: center;"&gt;\(r_{22}\)&lt;/td&gt;
&lt;td style="text-align: center;"&gt;\(r_{23}\)&lt;/td&gt;
&lt;td style="text-align: center;"&gt;\(r_{24}\)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;So, for the second row,
\(r_{21} = k_{12} i_{11} + k_{22} i_{21} \), move one over,
\(r_{22} = k_{11} i_{11} + k_{12} i_{12} + k_{21} i_{21} + k_{22} i_{22} \), one more to the right,
\(r_{23} = k_{11}i_{12} + k_{12} i_{13} + k_{21} i_{22} + k_{22} i_{23} \), and finally
\(r_{24} = k_{11}i_{13} + k_{21} i_{23} \).&lt;/p&gt;
&lt;p&gt;It works similar for the remaining two rows.&lt;/p&gt;
&lt;p&gt;If we unroll the layer \(I\) row-wise into a column vector \(I_\text{col}\),
\[
    I_\text{col} = 
    \left(
    \matrix{ 
        i_{11} \cr
        i_{12} \cr
        i_{13} \cr
        i_{21} \cr
        i_{22} \cr
        i_{23} \cr
        i_{31} \cr
        i_{32} \cr
        i_{33}
        }
    \right),
\]
then we can express this as a matrix-vector multiplication of a matrix formed from the entries of the kernel \(K\) and the vector\(I_\text{col}\), 
\[
\left(\matrix{
k_{22} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \cr
k_{21} &amp;amp; k_{22} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \cr
0 &amp;amp; k_{21} &amp;amp; k_{22} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \cr
0 &amp;amp; 0 &amp;amp; k_{21} &amp;amp; k_{22} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \cr
k_{12} &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{22} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \cr
k_{11} &amp;amp; k_{12} &amp;amp; 0 &amp;amp; k_{21} &amp;amp; k_{22} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \cr
0 &amp;amp; k_{11} &amp;amp; k_{12} &amp;amp; 0 &amp;amp; k_{21} &amp;amp; k_{22} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \cr
0 &amp;amp; 0 &amp;amp; k_{11} &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{21} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \cr
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{12} &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{22} &amp;amp; 0 &amp;amp; 0 \cr
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{11} &amp;amp; k_{12} &amp;amp; 0 &amp;amp; k_{21} &amp;amp; k_{22} &amp;amp; 0 \cr
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{11} &amp;amp; k_{12} &amp;amp; 0 &amp;amp; k_{21} &amp;amp; k_{22} \cr
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{11} &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{21} \cr
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{12} &amp;amp; 0 &amp;amp; 0 \cr
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{11} &amp;amp; k_{12} &amp;amp; 0 \cr
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{11} &amp;amp; k_{12} \cr
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; k_{11} }\right)
\cdot
\left(\matrix{
i_{11} \cr
i_{12} \cr
i_{13} \cr
i_{21} \cr
i_{22} \cr
i_{23} \cr
i_{31} \cr
i_{32} \cr
i_{33}} 
\right)
\]&lt;/p&gt;
&lt;p&gt;Now one can already see that the matrix formed from the kernel entries has a very peculiar shape - the shape of a doubly blocked Toeplitz matrix&lt;/p&gt;
&lt;h3&gt;Doubly blocked Toeplitz matrix&lt;/h3&gt;
&lt;p&gt;A Toeplitz matrix is a matrix where the values along all diagonals are constant, i.e.&lt;/p&gt;
&lt;p&gt;\[
\left(
    \matrix{ 
        a_{0} &amp;amp; a_{-1} &amp;amp; a_{-2} &amp;amp; \cdots  &amp;amp; \cdots &amp;amp; \cdots &amp;amp; a_{-(N-1)} \cr
        a_{1} &amp;amp; a_{0} &amp;amp; a_{-1} &amp;amp; a_{-2} &amp;amp;  &amp;amp; &amp;amp; \vdots \cr
        a_{2} &amp;amp; a_{1} &amp;amp; a_{0} &amp;amp; a_{-1} &amp;amp;  &amp;amp; &amp;amp; \vdots \cr
        \vdots &amp;amp; \ddots &amp;amp; \ddots &amp;amp; \ddots &amp;amp; \ddots &amp;amp; \ddots &amp;amp; &amp;amp; \vdots \cr
        \vdots &amp;amp; &amp;amp; &amp;amp; \ddots  &amp;amp; a_{0} &amp;amp; a_{-1} &amp;amp; a_{-2} \cr
        \vdots &amp;amp; &amp;amp; &amp;amp;  &amp;amp; a_{1} &amp;amp; a_{0} &amp;amp; a_{-1} \cr
        a_{M-1} &amp;amp; \cdots  &amp;amp; \cdots &amp;amp; \cdots &amp;amp; a_{2} &amp;amp; a_{1} &amp;amp; a_{0} }
    \right) .
\]&lt;/p&gt;
&lt;p&gt;Furthermore, if we build a matrix \(A\) out of Toeplitz sub-matrices \(A_{k}\) &lt;em&gt;and&lt;/em&gt; the structure of \(A\) with respect to these submatrices is also Toeplitz:&lt;/p&gt;
&lt;p&gt;\[
    A = \left(
    \matrix{ 
        A_{0} &amp;amp; A_{-1} &amp;amp; \cdots &amp;amp; A_{-(L-1)} \cr
        A_{1} &amp;amp; A_{0} &amp;amp; \cdots &amp;amp; A_{-(L-2)} \cr
        \vdots &amp;amp; \vdots &amp;amp; \ddots &amp;amp; \vdots \cr
        A_{K} &amp;amp; A_{K-1} &amp;amp; \cdots &amp;amp; A_{0}}
    \right),
\]&lt;/p&gt;
&lt;p&gt;then, this matrix is called a doubly-blocked Toeplitz matrix. A standard way to generate a Toeplitz matrix from a vector \(v\) is to use \(v\) as the first column vector, then make one cyclic permutation and use it as the second column vector and so on.&lt;/p&gt;
&lt;h3&gt;The method&lt;/h3&gt;
&lt;p&gt;As we have seen on the example above, 2D convolution operations can be expressed as multiplication by a doubly-blocked Toeplitz matrix. As a general method, applied to the example above,
to convolve \(K\) with \(I\), we first flip \(K\) across the horizontal and vertical axis and pad it to the output size \((I_\text{height} + K_\text{height} - 1) \times (I_\text{width} + K_\text{width} - 1)\) of the convolution.
For instance, here, the \(3 \times 3\) layer \(I\) covolved by \(K\) above, leads to output size \(4 \times 4\).
Depending on the padding mode used by the convolution, typically, only part of this output is actually required.
The flipped and padded kernel \(K\) from above is
\[
    K_\text{pad}=
    \left(
    \matrix{ 
        k_{22} &amp;amp; k_{21} &amp;amp; 0 &amp;amp; 0 \cr
        k_{12} &amp;amp; k_{11} &amp;amp; 0 &amp;amp; 0 \cr
        0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \cr
        0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 }
    \right)
\]&lt;/p&gt;
&lt;p&gt;We then convert each &lt;em&gt;row vector&lt;/em&gt; of this matrix into Toeplitz matrices \(F_i\) as described above:
\[
    F_0=
    \left(
    \matrix{ 
        k_{22} &amp;amp; 0 &amp;amp; 0 \cr
        k_{21} &amp;amp; k_{22} &amp;amp; 0 \cr
        0 &amp;amp; k_{21} &amp;amp; k_{22} \cr
        0 &amp;amp; 0 &amp;amp; k_{21}}
    \right)
    \quad
    F_1=
    \left(
    \matrix{ 
        k_{12} &amp;amp; 0 &amp;amp;  0 \cr
        k_{11} &amp;amp; k_{12} &amp;amp; 0 \cr
        0 &amp;amp;  k_{11} &amp;amp; k_{12} \cr
        0 &amp;amp;  0 &amp;amp;  k_{11}}
    \right)
    \]
    \[
    F_2=
    \left(
    \matrix{ 
        0 &amp;amp; 0  &amp;amp; 0 \cr
        0 &amp;amp; 0 &amp;amp; 0 \cr
        0  &amp;amp; 0 &amp;amp; 0 \cr
        0  &amp;amp; 0  &amp;amp; 0}
    \right)
    \quad
    F_3=
    \left(
    \matrix{ 
        0 &amp;amp; 0  &amp;amp; 0 \cr
        0 &amp;amp; 0 &amp;amp; 0 \cr
        0  &amp;amp; 0 &amp;amp; 0 \cr
        0  &amp;amp; 0  &amp;amp; 0}
    \right)
\]
and, finally, assemble these into a doubly blocked Toeplitz matrix \(F\):&lt;/p&gt;
&lt;p&gt;\[
    F=
    \left(
    \matrix{ 
        F_0 &amp;amp; F_3 &amp;amp; F_2 \cr
        F_1 &amp;amp; F_0 &amp;amp; F_3 \cr
        F_2 &amp;amp; F_1 &amp;amp; F_0 \cr
        F_3 &amp;amp; F_2 &amp;amp; F_1
    }
    \right)
\]&lt;/p&gt;
&lt;p&gt;The convolution of \(K\) with \(I\)
is then given by multiplying F from the left onto \(I_\text{col}\) as defined above,
\[
  R_{\text{col}} = F \cdot I  \quad 
  \Leftrightarrow \quad R_{\text{col},j}= \sum_i F_{ji}I_i 
  \]&lt;/p&gt;
&lt;p&gt;Finally, \(R_{\text{col}}\) can be reinterpreted as the output matrix \(R\) by arranging its entries row-wise in a \(4\times 4\) matrix.&lt;/p&gt;
&lt;p&gt;There we have it - convolution (in the machine learning sense, i.e. corss-correlation) of a kernel \(K\) with a layer \(I\) expressed as the product of a doubly blocked Toeplitz matrix derived from \(K\) with the column vector of the row-wise unrolled entries from \(I\).&lt;/p&gt;
&lt;p&gt;The following python function is a simple implementation of this method&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nn"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;scipy.linalg&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;toeplitz&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;convolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# flip the kernel&lt;/span&gt;
    &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fliplr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flipud&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="c1"&gt;# calculate sizes&lt;/span&gt;
    &lt;span class="n"&gt;K_row_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K_col_num&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;
    &lt;span class="n"&gt;I_row_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;I_col_num&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;
    &lt;span class="n"&gt;R_row_num&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;K_row_num&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I_row_num&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;R_col_num&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;K_col_num&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;I_col_num&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="c1"&gt;# pad the kernel&lt;/span&gt;
    &lt;span class="n"&gt;K_pad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;R_row_num&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;K_row_num&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;R_col_num&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;K_col_num&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; 
                  &lt;span class="s1"&gt;'constant'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;constant_values&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"padded kernel= &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K_pad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Assemble the list of Toeplitz matrices F_i&lt;/span&gt;
    &lt;span class="n"&gt;toeplitz_list&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;R_row_num&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;K_pad&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,:]&lt;/span&gt;
        &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I_col_num&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;toeplitz_list&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toeplitz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Toeplitz list= &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;toeplitz_list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# make a matrix with the indices of the block F_i &lt;/span&gt;
    &lt;span class="c1"&gt;# of the doubly blocked Toeplitz matrix&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;R_row_num&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;r_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;doubly_indices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toeplitz&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"doubly_indices= &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doubly_indices&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# assemble the doubly blocked toeplitz matrix&lt;/span&gt;
    &lt;span class="n"&gt;toeplitz_m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;R_row_num&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I_row_num&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toeplitz_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;doubly_indices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
        &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hstack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;toeplitz_m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;toeplitz_m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toeplitz_m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Toeplitz matrix= &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;toeplitz_m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# make layer into column vector&lt;/span&gt;
    &lt;span class="n"&gt;I_col&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"I_col= "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;I_col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;matmul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;toeplitz_m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;I_col&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'R as vector= &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reshape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;R_row_num&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;R_col_num&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; 
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'R as matrix= &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To test, one can, for instance, use&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# kernel&lt;/span&gt;
&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;span class="c1"&gt;# layer&lt;/span&gt;
&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The output would then be&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;padded kernel= 
 [[40 30  0  0]
 [20 10  0  0]
 [ 0  0  0  0]]
Toeplitz list= 
 [array([[40.,  0.,  0.],
       [30., 40.,  0.],
       [ 0., 30., 40.],
       [ 0.,  0., 30.]]), array([[20.,  0.,  0.],
       [10., 20.,  0.],
       [ 0., 10., 20.],
       [ 0.,  0., 10.]]), array([[0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.],
       [0., 0., 0.]])]
doubly_indices= 
 [[0 2]
 [1 0]
 [2 1]]
Toeplitz matrix= 
 [[40.  0.  0.  0.  0.  0.]
 [30. 40.  0.  0.  0.  0.]
 [ 0. 30. 40.  0.  0.  0.]
 [ 0.  0. 30.  0.  0.  0.]
 [20.  0.  0. 40.  0.  0.]
 [10. 20.  0. 30. 40.  0.]
 [ 0. 10. 20.  0. 30. 40.]
 [ 0.  0. 10.  0.  0. 30.]
 [ 0.  0.  0. 20.  0.  0.]
 [ 0.  0.  0. 10. 20.  0.]
 [ 0.  0.  0.  0. 10. 20.]
 [ 0.  0.  0.  0.  0. 10.]]
I_col=  [1 2 3 4 5 6]
R as vector= 
 [ 40. 110. 180.  90. 180. 370. 470. 210.  80. 140. 170.  60.]
R as matrix= 
 [[ 40. 110. 180.  90.]
 [180. 370. 470. 210.]
 [ 80. 140. 170.  60.]]
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note, that this example is inspired by &lt;a href="https://raw.githubusercontent.com/alisaaalehi/convolution_as_multiplication/master/ConvAsMulExplained.pdf"&gt;Salehi's tutorial&lt;/a&gt; but because we are calculating the machine learning covolution (cross-correlation) and Salehi the mathematical convolution as used in signal processing, the results are not the same. To generate identical results one can use the doubly flipped kernel,&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# kernel&lt;/span&gt;
&lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;span class="c1"&gt;# layer&lt;/span&gt;
&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;convolution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;K&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"R= &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and obtain&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; 
 &lt;span class="p"&gt;[[&lt;/span&gt; &lt;span class="mf"&gt;10.&lt;/span&gt;  &lt;span class="mf"&gt;40.&lt;/span&gt;  &lt;span class="mf"&gt;70.&lt;/span&gt;  &lt;span class="mf"&gt;60.&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="mf"&gt;70.&lt;/span&gt; &lt;span class="mf"&gt;230.&lt;/span&gt; &lt;span class="mf"&gt;330.&lt;/span&gt; &lt;span class="mf"&gt;240.&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
 &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;120.&lt;/span&gt; &lt;span class="mf"&gt;310.&lt;/span&gt; &lt;span class="mf"&gt;380.&lt;/span&gt; &lt;span class="mf"&gt;240.&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;which exactly is Salehi's result.&lt;/p&gt;
&lt;div class="footnote"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Convolution images created with software from:
Vincent Dumoulin and Francesco Visin, A guide to convolution arithmetic for deep learning (2016) ArXiv e-prints 1603.07285; &lt;a href="https://github.com/vdumoulin/conv_arithmetic"&gt;Software on github&lt;/a&gt; &lt;a class="footnote-backref" href="http://genn-team.github.io/posts/sw_blog_toeplitz.html#fnref:1" title="Jump back to footnote 1 in the text"&gt;↩&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description><category>convolution</category><category>math</category><category>toeplitz</category><guid>http://genn-team.github.io/posts/sw_blog_toeplitz.html</guid><pubDate>Tue, 21 Dec 2021 14:39:44 GMT</pubDate></item></channel></rss>