diff --git a/doc/docs/Backend_Hooks.md b/doc/docs/Backend_Hooks.md new file mode 100644 index 000000000..11869cbc8 --- /dev/null +++ b/doc/docs/Backend_Hooks.md @@ -0,0 +1,166 @@ +--- +# Backend Hooks +--- + +`` declares a small extension point that lets an external library plug into meep's hot paths at load time, without forking or patching upstream sources. The intended use is sibling backends (CUDA, ROCm, vectorized-CPU) that ship as separate shared libraries pinned to a specific meep version. + +This document describes the contract. The header itself is the canonical source. + +## Shape + +A single process-global table of function pointers: + +```cpp +namespace meep { +struct backend_hooks { + void (*init) (fields *f); + void (*cleanup) (fields *f); + bool (*step) (fields *f); + void (*sync_to_host) (fields *f); + void (*sync_from_host) (fields *f); + realnum (*read_point) (const fields *f, const fields_chunk *fc, + component c, int cmp, ptrdiff_t idx); + bool (*needs_host_sync)(const fields *f); +}; +extern backend_hooks meep_backend; +} +``` + +All entries default to null. **Null means "fall through to the in-tree CPU implementation."** Every call site in upstream meep is a null-pointer test, so a meep build with no backend loaded behaves bit-identically to a build without these hooks at all. + +A backend is installed by writing function pointers into `meep::meep_backend` at library load time (typically from a `__attribute__((constructor))`). + +## Per-sim opaque state + +Backends store per-simulation state in `fields::backend_state` (a `void *`) and per-chunk state in `fields_chunk::backend_state`. Upstream meep never inspects these slots; backends are responsible for allocating them in `init` and freeing them in `cleanup`. + +## Lifecycle + +``` +fields::fields(...) # constructs structure + chunks + connections + └─ meep_backend.init(this) # backend allocates per-sim/chunk state + # backend stashes pointers in backend_state slots +... user code calls f.step(), f.flux_in_box(), f.add_dft(), etc. ... +fields::~fields() + └─ meep_backend.cleanup(this) # called BEFORE chunks are deleted, so the + # backend can read fields_chunk::backend_state + └─ chunk teardown +``` + +`init` is also called from the `fields` copy constructor, so a backend that supports `fields(const fields&)` must be prepared to attach to a freshly-copied object. + +## Hook contracts + +### `step` + +```cpp +bool step(fields *f); +``` + +Take one full FDTD timestep on behalf of the caller. Returning `true` means the backend handled the step and the in-tree CPU step path is skipped. Returning `false` (or leaving the hook null) falls through to the CPU step path -- useful for backends that handle most configurations but not all. + +The step hook is **bypassed entirely** when `fields::backend_suspended == true`. The CW solver sets this flag for the duration of its CG iterations, since it operates against the host arrays directly. + +### `sync_to_host` / `sync_from_host` + +```cpp +void sync_to_host(fields *f); +void sync_from_host(fields *f); +``` + +Sync the canonical host arrays (`fc->f[c][cmp]`) with the backend's shadow storage. + +- `sync_to_host`: bring the latest field values into the host arrays so CPU code can read them. Called at every CPU readout site (DFT, monitors, integration, dump, CW solver). +- `sync_from_host`: push host-side modifications back into shadow storage. Called after `fields::load`, after `fields::solve_cw`. + +Backends may treat either as a no-op when not active. Both hooks are also called via the convenience helper `sync_host_if_needed(f)`, which checks `needs_host_sync` first. + +### `needs_host_sync` + +```cpp +bool needs_host_sync(const fields *f); +``` + +Returns `true` iff the host arrays do not reflect the backend's current state and `sync_to_host` should be called before reading them. Returns `false` when no backend is active or when the backend's host arrays are already in sync. + +### `read_point` + +```cpp +realnum read_point(const fields *f, const fields_chunk *fc, + component c, int cmp, ptrdiff_t idx); +``` + +Fast single-cell read. Used on the LDOS / point-monitor hot path to fetch one field value without triggering a full `sync_to_host`. Backends that don't implement this should leave it null; callers fall back to a sync followed by a direct array read. + +### `init` / `cleanup` + +```cpp +void init(fields *f); +void cleanup(fields *f); +``` + +Per-sim setup and teardown. Typical body: allocate device buffers and per-chunk shadow storage, stash pointers in `f->backend_state` and each `f->chunks[i]->backend_state`. The matching `cleanup` releases everything. + +## Minimal example + +A "transparent" backend that counts hook invocations and defers all work to the CPU. Suitable as a starting skeleton. + +```cpp +#include +#include + +namespace my_backend { + +static void on_init(meep::fields *) { /* allocate device state, stash in f->backend_state */ } +static void on_cleanup(meep::fields *) { /* free device state */ } + +static bool on_step(meep::fields *) { + // run the FDTD step on the device + // return true to skip the CPU path; return false to defer to CPU + return false; +} + +static void on_sync_to_host(meep::fields *) { /* device -> host arrays */ } +static void on_sync_from_host(meep::fields *) { /* host arrays -> device */ } +static bool on_needs_host_sync(const meep::fields *) { return false; } + +__attribute__((constructor)) +static void install() { + meep::meep_backend.init = on_init; + meep::meep_backend.cleanup = on_cleanup; + meep::meep_backend.step = on_step; + meep::meep_backend.sync_to_host = on_sync_to_host; + meep::meep_backend.sync_from_host = on_sync_from_host; + meep::meep_backend.needs_host_sync = on_needs_host_sync; +} + +} // namespace my_backend +``` + +Build it as a shared library, then load it before running meep: + +```sh +LD_PRELOAD=libmy_backend.so python -c "import meep; ..." +``` + +See `tests/backend_hooks.cpp` in the meep source for a complete working example used as a CI guard. + +## MPI + +The hook table is process-global. Each MPI rank has its own `meep_backend` and calls hooks against its own `fields` object. The backend is responsible for any cross-rank coordination (NCCL, MPI, `sum_to_all`, etc.) — meep never synchronizes hook invocations across ranks. + +A few implicit contracts that backends running under MPI must respect: + +1. **`step` return value must be collective.** Every rank's `meep_backend.step(this)` must return the same `bool`. If one rank returns `true` (skip CPU path) and another returns `false`, the latter will execute `step_boundaries` -- which calls `MPI_Sendrecv` -- while the former will not, deadlocking the run. A backend that handles some configurations but not others must agree on that decision across ranks before returning, or just always return `true` once installed. + +2. **`backend_suspended` must be set/cleared collectively.** Same reason: if some ranks are suspended and others aren't, the next `step()` call diverges. In practice this is fine for `solve_cw` because the CW solver itself is collective. + +3. **`init` and `cleanup` run in collective contexts.** `fields::fields()` and `fields::~fields()` are collective in meep, so any MPI/NCCL collective the backend wants to do during setup or teardown (e.g., `MPI_Comm_split`, `ncclCommInitAll`) is safe to perform there. + +4. **`read_point` is local-only.** The in-tree call sites only invoke it when `chunks[i]->is_mine()` is true, so the backend only has to serve points it owns. Cross-rank queries fall back to the existing `chunks[i]->get_field()` path, which returns 0 on remote ranks and gets reduced via `sum_to_all` outside the loop. + +`sync_to_host` and `sync_from_host` are per-rank: each rank syncs its own chunks. They don't need to be collective unless the backend specifically wants them to be. + +## ABI notes + +There is no formal ABI versioning on the hook table. Backends should be pinned to a specific meep version (typically by submodule or distro package). Adding a new function pointer at the end of `backend_hooks` is forward-compatible with already-built backends because the global is zero-initialized; reordering or removing fields breaks ABI. diff --git a/src/Makefile.am b/src/Makefile.am index 8b545b333..892aa9fae 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,15 +1,16 @@ lib_LTLIBRARIES = libmeep.la include_HEADERS = meep.hpp -pkginclude_HEADERS = meep/mympi.hpp meep/vec.hpp meep/meep-config.h meepgeom.hpp material_data.hpp adjust_verbosity.hpp +pkginclude_HEADERS = meep/mympi.hpp meep/vec.hpp meep/meep-config.h meep/backend_hooks.hpp meepgeom.hpp material_data.hpp adjust_verbosity.hpp AM_CPPFLAGS = -I$(top_srcdir)/src BUILT_SOURCES = sphere-quad.h step_generic_stride1.cpp meep/meep-config.h -HDRS = meep.hpp meep_internals.hpp meep/mympi.hpp meep/vec.hpp \ +HDRS = meep.hpp meep_internals.hpp meep/mympi.hpp meep/vec.hpp meep/backend_hooks.hpp \ bicgstab.hpp meepgeom.hpp material_data.hpp adjust_verbosity.hpp libmeep_la_SOURCES = array_slice.cpp anisotropic_averaging.cpp \ +backend_hooks.cpp \ bands.cpp boundaries.cpp bicgstab.cpp casimir.cpp \ cw_fields.cpp dft.cpp dft_ldos.cpp energy_and_flux.cpp \ fields.cpp fields_dump.cpp fix_boundary_sources.cpp loop_in_chunks.cpp h5fields.cpp h5file.cpp \ diff --git a/src/backend_hooks.cpp b/src/backend_hooks.cpp new file mode 100644 index 000000000..9c63e3405 --- /dev/null +++ b/src/backend_hooks.cpp @@ -0,0 +1,23 @@ +/* Copyright (C) 2005-2026 Massachusetts Institute of Technology +% +% This program is free software; you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation; either version 2, or (at your option) +% any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +*/ + +#include "meep/backend_hooks.hpp" + +namespace meep { + +/* Process-global table. Zero-initialized: every hook starts as a null + * function pointer, which the call sites read as "no backend installed, + * fall through to the CPU path". */ +backend_hooks meep_backend = {}; + +} /* namespace meep */ diff --git a/src/cw_fields.cpp b/src/cw_fields.cpp index 57e2a024a..4d5464c3a 100644 --- a/src/cw_fields.cpp +++ b/src/cw_fields.cpp @@ -16,6 +16,7 @@ */ #include "meep_internals.hpp" +#include "meep/backend_hooks.hpp" #include "bicgstab.hpp" using namespace std; @@ -146,6 +147,14 @@ bool fields::solve_cw(double tol, int maxiters, complex frequency, int L int tsave = t; // save time (gets incremented by iterations) int iters; + /* The CW solver runs CG iterations against the host-side `f[c][cmp]` + * arrays directly. If a backend is steering this sim, pull its shadow + * state back to host now and suspend the step hook so `step()` calls + * inside the CG loop run on the host. We unsuspend and push the + * converged solution back to the backend at the end. */ + sync_host_if_needed(this); + backend_suspended = true; + set_solve_cw_omega(2 * pi * frequency); step(); // step once to make sure everything is allocated @@ -248,6 +257,12 @@ bool fields::solve_cw(double tol, int maxiters, complex frequency, int L unset_solve_cw_omega(); update_dfts(); + /* Unsuspend and push the converged host-side solution back into the + * backend's shadow storage before normal time-stepping resumes. + * No-op without a backend. */ + backend_suspended = false; + if (meep_backend.sync_from_host) meep_backend.sync_from_host(this); + return !ierr; } diff --git a/src/dft.cpp b/src/dft.cpp index 0257586e6..c805b7c01 100644 --- a/src/dft.cpp +++ b/src/dft.cpp @@ -22,6 +22,7 @@ #include #include #include "meep.hpp" +#include "meep/backend_hooks.hpp" #include "meep_internals.hpp" using namespace std; @@ -312,6 +313,9 @@ void dft_chunk::update_dft(double time) { (Collective operation.) */ double fields::dft_norm() { am_now_working_on(Other); + /* Backends keep DFT accumulators on the device too — pull them back + * before reading. No-op when no backend is loaded. */ + sync_host_if_needed(this); double sum = 0.0; for (int i = 0; i < num_chunks; i++) if (chunks[i]->is_mine()) sum += chunks[i]->dft_norm2(gv); diff --git a/src/dft_ldos.cpp b/src/dft_ldos.cpp index 44d08bc71..5aeb18c25 100644 --- a/src/dft_ldos.cpp +++ b/src/dft_ldos.cpp @@ -16,6 +16,7 @@ */ #include "meep.hpp" +#include "meep/backend_hooks.hpp" #include "meep_internals.hpp" using namespace std; @@ -104,44 +105,53 @@ void dft_ldos::update(fields &f) { // ...don't worry about the tiny inefficiency of recomputing this repeatedly Jsum = 0.0; + /* If no fast point-read is available, fall back to a single up-front + * sync so the direct array reads (via read_field_at) see fresh data. */ + if (!meep_backend.read_point) sync_host_if_needed(&f); + for (int ic = 0; ic < f.num_chunks; ic++) if (f.chunks[ic]->is_mine()) { - for (const src_vol &sv : f.chunks[ic]->get_sources(D_stuff)) { + fields_chunk *fc = f.chunks[ic]; + for (const src_vol &sv : fc->get_sources(D_stuff)) { component c = direction_component(Ex, component_direction(sv.c)); - realnum *fr = f.chunks[ic]->f[c][0]; - realnum *fi = f.chunks[ic]->f[c][1]; + realnum *fr = fc->f[c][0]; + realnum *fi = fc->f[c][1]; if (fr && fi) // complex E for (size_t j = 0; j < sv.num_points(); j++) { const ptrdiff_t idx = sv.index_at(j); const complex &A = sv.amplitude_at(j); - EJ += complex(fr[idx], fi[idx]) * conj(A); + EJ += complex(read_field_at(&f, fc, c, 0, idx), + read_field_at(&f, fc, c, 1, idx)) * + conj(A); Jsum += abs(A); } else if (fr) { // E is purely real for (size_t j = 0; j < sv.num_points(); j++) { const ptrdiff_t idx = sv.index_at(j); const complex &A = sv.amplitude_at(j); - EJ += double(fr[idx]) * conj(A); + EJ += double(read_field_at(&f, fc, c, 0, idx)) * conj(A); Jsum += abs(A); } } } - for (const src_vol &sv : f.chunks[ic]->get_sources(B_stuff)) { + for (const src_vol &sv : fc->get_sources(B_stuff)) { component c = direction_component(Hx, component_direction(sv.c)); - realnum *fr = f.chunks[ic]->f[c][0]; - realnum *fi = f.chunks[ic]->f[c][1]; + realnum *fr = fc->f[c][0]; + realnum *fi = fc->f[c][1]; if (fr && fi) // complex H for (size_t j = 0; j < sv.num_points(); j++) { const ptrdiff_t idx = sv.index_at(j); const complex &A = sv.amplitude_at(j); - HJ += complex(fr[idx], fi[idx]) * conj(A); + HJ += complex(read_field_at(&f, fc, c, 0, idx), + read_field_at(&f, fc, c, 1, idx)) * + conj(A); Jsum += abs(A); } else if (fr) { // H is purely real for (size_t j = 0; j < sv.num_points(); j++) { const ptrdiff_t idx = sv.index_at(j); const complex &A = sv.amplitude_at(j); - HJ += double(fr[idx]) * conj(A); + HJ += double(read_field_at(&f, fc, c, 0, idx)) * conj(A); Jsum += abs(A); } } diff --git a/src/fields.cpp b/src/fields.cpp index 3e77b1d7b..f7aed8234 100644 --- a/src/fields.cpp +++ b/src/fields.cpp @@ -23,6 +23,7 @@ #include #include "meep.hpp" +#include "meep/backend_hooks.hpp" #include "meep_internals.hpp" using namespace std; @@ -83,6 +84,9 @@ fields::fields(structure *s, double m, double beta, bool zero_fields_near_cylori s->user_volume.num_direction(d) == 1) use_bloch(d, 0.0); } + + // Notify any installed backend that this fields object is ready. + if (meep_backend.init) meep_backend.init(this); } fields::fields(const fields &thef) @@ -125,9 +129,15 @@ fields::fields(const fields &thef) FOR_DIRECTIONS(d) { boundaries[b][d] = thef.boundaries[b][d]; } chunk_connections_valid = false; changed_materials = true; + + // Notify any installed backend about the copied-in fields. + if (meep_backend.init) meep_backend.init(this); } fields::~fields() { + // Let the backend release per-sim and per-chunk state while the chunks + // are still alive (the backend may want to read fields_chunk::backend_state). + if (meep_backend.cleanup) meep_backend.cleanup(this); for (int i = 0; i < num_chunks; i++) delete chunks[i]; delete[] chunks; diff --git a/src/fields_dump.cpp b/src/fields_dump.cpp index 08b1bf99c..124874d65 100644 --- a/src/fields_dump.cpp +++ b/src/fields_dump.cpp @@ -26,6 +26,7 @@ #include #include "meep.hpp" +#include "meep/backend_hooks.hpp" #include "meep_internals.hpp" namespace meep { @@ -110,6 +111,10 @@ void fields::dump(const char *filename, bool single_parallel_file) { printf("creating fields output file \"%s\" (%d)...\n", filename, single_parallel_file); } + /* Make sure host arrays match the backend's shadow state before we + * serialize them. No-op when no backend is loaded. */ + sync_host_if_needed(this); + h5file file(filename, h5file::WRITE, single_parallel_file, !single_parallel_file); // Write out the current time 't' @@ -275,6 +280,10 @@ void fields::load(const char *filename, bool single_parallel_file) { load_dft_hdf5(chunks[i]->dft_chunks, dataname, &file, 0, single_parallel_file); } } + + /* Push the freshly-loaded host arrays back into the backend's shadow + * storage so the next step sees them. No-op when no backend is loaded. */ + if (meep_backend.sync_from_host) meep_backend.sync_from_host(this); } } // namespace meep diff --git a/src/loop_in_chunks.cpp b/src/loop_in_chunks.cpp index 484a789d4..bb6fd47be 100644 --- a/src/loop_in_chunks.cpp +++ b/src/loop_in_chunks.cpp @@ -21,6 +21,7 @@ #include #include "meep.hpp" +#include "meep/backend_hooks.hpp" #include "meep_internals.hpp" /* This file contains a generic function for looping over all of the @@ -338,6 +339,11 @@ void compute_boundary_weights(grid_volume gv, const volume &where, ivec &is, ive void fields::loop_in_chunks(field_chunkloop chunkloop, void *chunkloop_data, const volume &where, component cgrid, bool use_symmetry, bool snap_empty_dimensions) { + /* If a backend is steering this sim, make sure the host-side `f[c][cmp]` + * arrays the chunkloop will read are up-to-date. No-op when no backend + * is loaded. */ + sync_host_if_needed(this); + if (coordinate_mismatch(gv.dim, cgrid)) meep::abort("Invalid fields::loop_in_chunks grid type %s for dimensions %s\n", component_name(cgrid), dimension_name(gv.dim)); diff --git a/src/meep.hpp b/src/meep.hpp index d9be5bccc..8abf13e8c 100644 --- a/src/meep.hpp +++ b/src/meep.hpp @@ -1526,6 +1526,11 @@ class fields_chunk { const char *outdir; int chunk_idx; + /* Opaque per-chunk slot owned by an external backend (see + * meep/backend_hooks.hpp). Default null; backends allocate in + * `backend_hooks::init` and free in `backend_hooks::cleanup`. */ + void *backend_state = nullptr; + fields_chunk(structure_chunk *, const char *outdir, double m, double beta, bool zero_fields_near_cylorigin, int chunkidx, int loop_tile_base_db, std::vector bfast_scaled_k); @@ -1766,6 +1771,15 @@ class fields { bool components_allocated; size_t loop_tile_base_db, loop_tile_base_eh; + /* Opaque per-sim slot owned by an external backend (see + * meep/backend_hooks.hpp). Default null. */ + void *backend_state = nullptr; + + /* When true, the backend's `step` hook is bypassed and the in-tree + * CPU step path runs instead. Used by the CW solver, which iterates + * directly against host arrays. */ + bool backend_suspended = false; + // fields.cpp methods: fields(structure *, double m = 0, double beta = 0, bool zero_fields_near_cylorigin = true, int loop_tile_base_db = 0, int loop_tile_base_eh = 0, diff --git a/src/meep/backend_hooks.hpp b/src/meep/backend_hooks.hpp new file mode 100644 index 000000000..75024e074 --- /dev/null +++ b/src/meep/backend_hooks.hpp @@ -0,0 +1,114 @@ +/* Copyright (C) 2005-2026 Massachusetts Institute of Technology +% +% This program is free software; you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation; either version 2, or (at your option) +% any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program; if not, write to the Free Software Foundation, +% Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +/* Backend extension point. + * + * Vanilla meep runs entirely on the CPU. This header declares a small + * table of function pointers that an external backend library (e.g. a + * CUDA, ROCm, or vectorized-CPU implementation) may install at load time + * to redirect hot paths through its own implementation. + * + * All entries default to null; null means "fall through to the in-tree + * CPU implementation", and every call site is written so that a null + * hook is a no-op. As a result, a stock build of meep with no backend + * loaded behaves bit-identically to one without these hooks. + * + * State management. Backends store per-fields state in + * `fields::backend_state` and per-chunk state in + * `fields_chunk::backend_state` (both `void*`, owned by the backend). + * Upstream meep never inspects these; backends are responsible for + * allocating them in `init` and freeing them in `cleanup`. + * + * Suspension. A backend can be suspended for a single `fields` object + * by setting `fields::backend_suspended = true`. The step hook will + * not fire while suspended; the in-tree CPU step path runs instead. + * Used by the CW solver, which iterates against host arrays directly. + */ + +#ifndef MEEP_BACKEND_HOOKS_H +#define MEEP_BACKEND_HOOKS_H + +#include "meep.hpp" /* fields, fields_chunk, realnum, component */ + +namespace meep { + +struct backend_hooks { + /* Per-sim lifecycle. `init` is called once after a `fields` object's + * structure is built and chunks are connected; `cleanup` is called + * once before the `fields` object is destroyed. Backends typically + * allocate/free per-sim shadow state here and stash a pointer in + * `fields::backend_state`. */ + void (*init)(fields *f); + void (*cleanup)(fields *f); + + /* Take one FDTD timestep on behalf of the caller. Returning `true` + * means the backend handled the step and the in-tree CPU step path + * should be skipped. Returning `false` (or leaving this null) falls + * through to the CPU step path. Skipped entirely while + * `fields::backend_suspended` is true. */ + bool (*step)(fields *f); + + /* Sync canonical host arrays with the backend's shadow storage. + * `sync_to_host` brings the latest field values into the per-chunk + * `f[c][cmp]` arrays so CPU code can read them; `sync_from_host` + * pushes any host-side modifications back into shadow storage so the + * next step sees them. Backends may treat either as a no-op when + * not active (e.g. before `init` has been called). Called by every + * CPU code path that reads or writes field data. */ + void (*sync_to_host)(fields *f); + void (*sync_from_host)(fields *f); + + /* Fast single-point read. Used on the LDOS / point-monitor hot path + * to fetch one field value without a full sync. Backends that do not + * implement this should leave it null; callers fall back to a full + * `sync_to_host` followed by a direct array read. */ + realnum (*read_point)(const fields *f, const fields_chunk *fc, component c, int cmp, + ptrdiff_t idx); + + /* Single predicate. Returns true iff the host arrays do not reflect + * the backend's current state and `sync_to_host` should be called + * before reading them. Returns false when no backend is active or + * when the backend's host arrays are already in sync. */ + bool (*needs_host_sync)(const fields *f); +}; + +/* Single process-global hook table. Default-initialized to all nulls. + * The plugin populates this at library load time. */ +extern backend_hooks meep_backend; + +/* Convenience used at every CPU readout site. Inlined so it compiles + * to a single null-pointer test (and nothing else) when no backend is + * loaded. */ +inline void sync_host_if_needed(fields *f) { + if (meep_backend.needs_host_sync && meep_backend.sync_to_host && meep_backend.needs_host_sync(f)) + meep_backend.sync_to_host(f); +} + +/* Read a single cell of `fc->f[c][cmp]`. If a backend has installed + * `read_point`, route through it (avoiding a full host sync); otherwise + * read the host array directly. The caller is responsible for ensuring + * `fc->f[c][cmp]` is non-null and (when no read_point hook is installed) + * for calling `sync_host_if_needed` once before the loop. Inlined to + * compile to a single load when no backend is loaded. */ +inline realnum read_field_at(const fields *f, const fields_chunk *fc, component c, int cmp, + ptrdiff_t idx) { + return meep_backend.read_point ? meep_backend.read_point(f, fc, c, cmp, idx) : fc->f[c][cmp][idx]; +} + +} /* namespace meep */ + +#endif /* MEEP_BACKEND_HOOKS_H */ diff --git a/src/monitor.cpp b/src/monitor.cpp index deb06f236..8461fe849 100644 --- a/src/monitor.cpp +++ b/src/monitor.cpp @@ -20,6 +20,7 @@ #include #include "meep.hpp" +#include "meep/backend_hooks.hpp" #include "meep_internals.hpp" #include "config.h" @@ -143,14 +144,30 @@ complex fields::get_field(component c, const vec &loc, bool parallel) co } complex fields::get_field(component c, const ivec &origloc, bool parallel) const { + /* This is the chunk-iterating leaf of the get_field overload set; every + * other overload eventually funnels through here. If no fast point-read + * is available, fall back to a single up-front sync so direct array + * reads (via read_field_at) see fresh data. */ + if (!meep_backend.read_point) sync_host_if_needed(const_cast(this)); + ivec iloc = origloc; complex kphase = 1.0; locate_point_in_user_volume(&iloc, &kphase); for (int sn = 0; sn < S.multiplicity(); sn++) for (int i = 0; i < num_chunks; i++) if (chunks[i]->gv.owns(S.transform(iloc, sn))) { - complex val = S.phase_shift(c, sn) * kphase * - chunks[i]->get_field(S.transform(c, sn), S.transform(iloc, sn)); + const component tc = S.transform(c, sn); + const ivec tiloc = S.transform(iloc, sn); + complex val; + if (chunks[i]->is_mine() && chunks[i]->f[tc][0]) { + const ptrdiff_t idx = chunks[i]->gv.index(tc, tiloc); + const realnum vr = read_field_at(this, chunks[i], tc, 0, idx); + const realnum vi = + chunks[i]->f[tc][1] ? read_field_at(this, chunks[i], tc, 1, idx) : realnum(0); + val = complex(vr, vi); + } + else { val = chunks[i]->get_field(tc, tiloc); } + val *= S.phase_shift(c, sn) * kphase; return parallel ? sum_to_all(val) : val; } return 0.0; diff --git a/src/step.cpp b/src/step.cpp index 8fb40dc4e..f11ffc80f 100644 --- a/src/step.cpp +++ b/src/step.cpp @@ -22,6 +22,7 @@ #include #include "meep.hpp" +#include "meep/backend_hooks.hpp" #include "meep_internals.hpp" #include "config.h" @@ -40,6 +41,13 @@ void fields::step() { restore_magnetic_fields(); } + // If a backend is installed and chooses to handle this step, hand off + // the entire timestep to it. Backends that cannot handle the current + // configuration may return false to fall through to the CPU path. + // Callers that need to suppress the backend (e.g. solve_cw, which + // runs on the host arrays) set `backend_suspended = true`. + if (!backend_suspended && meep_backend.step && meep_backend.step(this)) return; + am_now_working_on(Stepping); if (!t) { diff --git a/tests/Makefile.am b/tests/Makefile.am index b4fc789f5..60e56b882 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -1,4 +1,4 @@ -SRC = aniso_disp.cpp bench.cpp bragg_transmission.cpp \ +SRC = aniso_disp.cpp backend_hooks.cpp bench.cpp bragg_transmission.cpp \ convergence_cyl_waveguide.cpp cylindrical.cpp dump_load.cpp flux.cpp \ harmonics.cpp integrate.cpp known_results.cpp near2far.cpp \ one_dimensional.cpp physical.cpp stress_tensor.cpp symmetry.cpp \ @@ -16,7 +16,7 @@ AM_CPPFLAGS = -I$(top_srcdir)/src -DDATADIR=\"$(srcdir)/\" .SUFFIXES = .dac .done -check_PROGRAMS = aniso_disp bench bragg_transmission convergence_cyl_waveguide cylindrical dump_load flux harmonics integrate known_results near2far one_dimensional physical stress_tensor symmetry three_d two_dimensional 2D_convergence h5test pml pw-source-ll ring-ll cyl-ellipsoid-ll absorber-1d-ll array-slice-ll user-defined-material dft-fields bend-flux-ll array-metadata +check_PROGRAMS = aniso_disp backend_hooks bench bragg_transmission convergence_cyl_waveguide cylindrical dump_load flux harmonics integrate known_results near2far one_dimensional physical stress_tensor symmetry three_d two_dimensional 2D_convergence h5test pml pw-source-ll ring-ll cyl-ellipsoid-ll absorber-1d-ll array-slice-ll user-defined-material dft-fields bend-flux-ll array-metadata if WITH_LIBGDSII check_PROGRAMS += gdsII-3d @@ -28,6 +28,9 @@ array_metadata_LDADD = $(MEEPLIBS) aniso_disp_SOURCES = aniso_disp.cpp aniso_disp_LDADD = $(MEEPLIBS) +backend_hooks_SOURCES = backend_hooks.cpp +backend_hooks_LDADD = $(MEEPLIBS) + bench_SOURCES = bench.cpp bench_LDADD = $(MEEPLIBS) @@ -114,7 +117,7 @@ user_defined_material_LDADD = $(MEEPLIBS) dist_noinst_DATA = cyl-ellipsoid-eps-ref.h5 array-slice-ll-ref.h5 gdsII-3d.gds -TESTS = aniso_disp bench bragg_transmission convergence_cyl_waveguide cylindrical dump_load flux harmonics integrate known_results near2far one_dimensional physical stress_tensor symmetry three_d two_dimensional 2D_convergence h5test pml +TESTS = aniso_disp backend_hooks bench bragg_transmission convergence_cyl_waveguide cylindrical dump_load flux harmonics integrate known_results near2far one_dimensional physical stress_tensor symmetry three_d two_dimensional 2D_convergence h5test pml if WITH_MPI LOG_COMPILER = $(RUNCODE) diff --git a/tests/backend_hooks.cpp b/tests/backend_hooks.cpp new file mode 100644 index 000000000..5eaa4ad65 --- /dev/null +++ b/tests/backend_hooks.cpp @@ -0,0 +1,145 @@ +/* Copyright (C) 2005-2026 Massachusetts Institute of Technology +% +% This program is free software; you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation; either version 2, or (at your option) +% any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +*/ + +/* Reference test for the backend extension-point hook surface + * (src/meep/backend_hooks.hpp). + * + * Asserts: + * 1. With a transparent counting backend installed (every hook + * defers to CPU), numerical results are bit-identical to a + * baseline run with no backend. This is the load-bearing + * claim: the hook surface itself never perturbs values. + * 2. Hooks fire at the expected sites with sensible counts. + */ + +#include +#include + +#include +#include + +using namespace meep; + +static double one(const vec &) { return 1.0; } + +namespace { + +struct hook_counts { + int init = 0; + int cleanup = 0; + int step = 0; + int sync_to_host = 0; + int sync_from_host = 0; + int needs_host_sync = 0; + int read_point = 0; +}; +hook_counts counts; + +void test_init(fields *) { counts.init++; } +void test_cleanup(fields *) { counts.cleanup++; } +bool test_step(fields *) { + counts.step++; + return false; /* fall through to CPU step path */ +} +void test_sync_to_host(fields *) { counts.sync_to_host++; } +void test_sync_from_host(fields *) { counts.sync_from_host++; } +bool test_needs_host_sync(const fields *) { + counts.needs_host_sync++; + return false; /* host always considered fresh -> sync_to_host never fires */ +} +realnum test_read_point(const fields *, const fields_chunk *, component, int, ptrdiff_t) { + counts.read_point++; + return 0; +} + +void install_transparent_backend() { + meep_backend.init = test_init; + meep_backend.cleanup = test_cleanup; + meep_backend.step = test_step; + meep_backend.sync_to_host = test_sync_to_host; + meep_backend.sync_from_host = test_sync_from_host; + meep_backend.needs_host_sync = test_needs_host_sync; + meep_backend.read_point = nullptr; /* leave null so dft_ldos uses the host path */ + counts = hook_counts{}; +} + +void uninstall_backend() { meep_backend = backend_hooks{}; } + +constexpr int n_steps = 100; + +double run_sim() { + grid_volume gv = volone(6.0, 10.0); + structure s(gv, one); + fields f(&s); + /* gaussian pulse: freq=0.7, fwidth=1.0, peak t=0, cutoff at 4 sigmas */ + f.add_point_source(Ex, 0.7, 1.0, 0.0, 4.0, vec(2.5), 1.0); + for (int i = 0; i < n_steps; i++) + f.step(); + return f.field_energy(); +} + +} /* namespace */ + +int main(int argc, char **argv) { + initialize mpi(argc, argv); + verbosity = 0; + + /* 1. Baseline: no backend installed. */ + uninstall_backend(); + const double baseline_energy = run_sim(); + + /* 2. Transparent backend: every hook fires but defers to CPU. */ + install_transparent_backend(); + const double hooked_energy = run_sim(); + uninstall_backend(); + + /* 3. Numerical results must be bit-identical. */ + if (baseline_energy != hooked_energy) { + master_printf("backend_hooks: FAIL baseline_energy=%.17g hooked_energy=%.17g (diff=%g)\n", + baseline_energy, hooked_energy, baseline_energy - hooked_energy); + return 1; + } + + /* 4. Hook fire counts. */ + if (counts.init != 1) { + master_printf("backend_hooks: FAIL init=%d expected 1\n", counts.init); + return 1; + } + if (counts.cleanup != 1) { + master_printf("backend_hooks: FAIL cleanup=%d expected 1\n", counts.cleanup); + return 1; + } + if (counts.step < n_steps) { + master_printf("backend_hooks: FAIL step=%d expected >= %d\n", counts.step, n_steps); + return 1; + } + /* needs_host_sync was reported as false everywhere, so sync_to_host + * must never have fired. */ + if (counts.sync_to_host != 0) { + master_printf("backend_hooks: FAIL sync_to_host=%d expected 0\n", counts.sync_to_host); + return 1; + } + if (counts.read_point != 0) { + master_printf("backend_hooks: FAIL read_point=%d (hook was null)\n", counts.read_point); + return 1; + } + /* needs_host_sync should be queried at least once per readout site. */ + if (counts.needs_host_sync < 1) { + master_printf("backend_hooks: FAIL needs_host_sync=%d expected >= 1\n", counts.needs_host_sync); + return 1; + } + + master_printf("backend_hooks: PASS (init=%d cleanup=%d step=%d needs_host_sync=%d)\n", + counts.init, counts.cleanup, counts.step, counts.needs_host_sync); + return 0; +}