Log System¶
Architecture and contracts for the host + device logging subsystem.
For the user-facing model — one Python knob and the CLI flags
— see testing.md § Log levels. This file
documents the implementation: layering, multi-.so singleton sharing, ABIs,
build wiring, and output formats.
Mental model¶
Python: logging.getLogger("simpler").setLevel(N)
│
▼ Worker.init() snapshots + normalizes the threshold once
│
ChipWorker.init(device_id, bins, level) ◀── Python wrapper
│
1. ctypes.CDLL(libsimpler_log.so, RTLD_GLOBAL) ◀── one HostLogger per process
2. simpler_log_init(level) ──→ HostLogger.set_level
(seeds HostLogger BEFORE any host_runtime /
sim_context / aicore SO is dlopen'd, so
any LOG_* macro firing during dlopen-time
constructors already sees the right filter)
3. ctypes.CDLL(libcpu_sim_context.so, RTLD_GLOBAL) (sim only)
4. _ChipWorker.init(host_lib, aicpu, aicore, device_id) ◀── C++
├─ dlopen libhost_runtime.so RTLD_LOCAL ──→ undefined HostLogger /
│ unified_log_* symbols
│ resolve via (1)
└─ simpler_init(ctx, device_id, aicpu*, aicore*)
──→ (onboard) dlog_setlevel(HostLogger.cann_level())
──→ attach thread + transfer executor binaries
Per device init:
runner reads HostLogger.level() ──→ InitArgs.log_level
AICPU receives the threshold
├─ sim: sets g_is_log_enable_* directly
└─ onboard: snapshots CANN CheckLogLevel() into g_is_log_enable_*
One threshold controls DEBUG / INFO / TIMING / WARN / ERROR; NUL suppresses
all output. The values match Python logging (10 / 20 / 25 / 30 / 40 / 60).
CANN has no TIMING level, so onboard setup maps both TIMING and WARN to CANN
WARN. The default TIMING threshold therefore keeps host [STRACE] markers
without opening CANN's INFO stream.
File layout¶
src/common/log/ ← libsimpler_log.so + public ABI
├── CMakeLists.txt libsimpler_log SHARED target
├── include/
│ ├── common/
│ │ ├── log_level.h shared level values + CANN mapping
│ │ └── unified_log.h public ABI (host AND device #include)
│ └── host_log.h HostLogger class (public — pto_runtime_c_api uses it)
├── host_log.cpp HostLogger impl
└── unified_log_host.cpp C ABI → HostLogger adapter
src/common/platform/ ← shared device-side log
├── include/aicpu/device_log.h low-level dev_vlog_* declarations
├── shared/aicpu/unified_log_device.cpp C ABI → dev_vlog_* adapter
├── onboard/aicpu/device_log.cpp onboard backend (CANN dlog)
└── sim/aicpu/device_log.cpp sim backend (fprintf to stderr)
Both architectures link these shared implementations into their platform
binaries; the per-architecture init kernels only forward InitArgs.log_level.
Three-layer ABI¶
Layer 1 — public macros (consumer-facing)¶
common/unified_log.h defines the only macros consumers should use:
LOG_DEBUG(fmt, ...)
LOG_INFO(fmt, ...)
LOG_TIMING(fmt, ...) // stable performance markers such as STRACE
LOG_WARN(fmt, ...)
LOG_ERROR(fmt, ...)
Each macro auto-injects [__FILENAME__:__LINE__] in front of the format
string and threads __FUNCTION__ as a separate argument.
Layer 2 — C ABI¶
The macros expand to five extern "C" functions declared in
common/unified_log.h:
void unified_log_error(const char *func, const char *fmt, ...);
void unified_log_warn (const char *func, const char *fmt, ...);
void unified_log_timing(const char *func, const char *fmt, ...);
void unified_log_info (const char *func, const char *fmt, ...);
void unified_log_debug(const char *func, const char *fmt, ...);
Two implementations link the same ABI symbols:
| Symbol owner | Implementation file | Backend |
|---|---|---|
libsimpler_log.so (host) |
src/common/log/unified_log_host.cpp |
HostLogger → stderr |
| AICPU binary (device) | src/common/platform/shared/aicpu/unified_log_device.cpp |
dev_vlog_* → backend |
The host .so is loaded with RTLD_GLOBAL so all consumer .sos
(host_runtime, cpu_sim_context, sim aicore_kernel, the binding) resolve
to the same HostLogger instance. The AICPU binary is independent — it
links its own copy of the ABI implementation that talks to dev_log_*
locally.
Layer 3 — backend primitives¶
Host (HostLogger in host_log.{h,cpp}):
class HostLogger {
public:
static HostLogger &get_instance(); // process-wide singleton
void set_level(LogLevel); // Python pushes via simpler_log_init
void vlog(LogLevel, func, fmt, va_list); // adapter entry point
int cann_level() const; // coarser CANN threshold
};
vlog is the single authority for level gating — the C ABI adapter does not
pre-check.
Device (dev_vlog_* in aicpu/device_log.h):
void dev_vlog_debug (const char *func, const char *fmt, va_list);
void dev_vlog_info (const char *func, const char *fmt, va_list);
void dev_vlog_timing(const char *func, const char *fmt, va_list);
void dev_vlog_warn (const char *func, const char *fmt, va_list);
void dev_vlog_error (const char *func, const char *fmt, va_list);
unified_log_device.cpp forwards the caller's va_list directly into
dev_vlog_* — no intermediate vsnprintf-to-buffer round-trip in this
layer. The sim backend is buffer-free (single vfprintf(stderr, ...)); the
onboard backend still buffers internally because CANN's dlog is variadic
only (no va_list variant).
Multi-.so singleton¶
The host log code lives in one .so (libsimpler_log.so) at
build/lib/libsimpler_log.so — process-global, not per arch or variant
(the source has zero arch-specific code, so a single shared copy per host
toolchain is sufficient). Every other .so that calls LOG_* resolves the
symbols against this single instance via RTLD_GLOBAL load order.
Load order — ChipWorker.init (Python wrapper) → _ChipWorker.init (C++)¶
# python/simpler/task_interface.py — ChipWorker.init(device_id, bins, level)
_preload_global(bins.simpler_log_path) # 1: ctypes.CDLL(RTLD_GLOBAL)
log_handle.simpler_log_init(level) # 2: seed HostLogger BEFORE
# any consumer SO is opened
if bins.sim_context_path:
_preload_global(bins.sim_context_path) # 3: ctypes.CDLL(RTLD_GLOBAL), sim only
self._impl.init(host_path, aicpu_path, aicore_path, device_id) # 4: C++ _ChipWorker.init
// src/common/worker/chip_worker.cpp — _ChipWorker.init(...)
handle = dlopen(host_lib_path, RTLD_NOW | RTLD_LOCAL); // 4a: undefined HostLogger /
// unified_log_* resolve via (1)
simpler_init_fn(device_ctx, device_id,
aicpu_bytes, aicpu_size,
aicore_bytes, aicore_size); // 4b: attach thread +
// transfer binaries +
// (onboard) sync dlog
_preload_global keeps a process-wide path → ctypes.CDLL registry so the
RTLD_GLOBAL load happens exactly once per path (mirrors the old C++
std::once_flag). _task_interface.so itself has no undefined HostLogger
symbols, so the preload only has to precede _ChipWorker.init, not module
import.
Each .so that needs the host symbols is built without compiling
host_log.cpp / unified_log_host.cpp. On macOS this requires
-undefined dynamic_lookup; on Linux undefined symbols in shared libraries
are allowed by default. CMake blocks live in:
src/{a5,a2a3}/platform/sim/host/CMakeLists.txtsrc/{a5,a2a3}/platform/sim/aicore/CMakeLists.txtsrc/common/platform/sim/sim_context/CMakeLists.txt
(Onboard host .so builds Linux-only and needs no flag.)
Verifying singleton sharing¶
cpu_sim_context.cpp::pto_cpu_sim_acquire_device emits a LOG_INFO
diagnostic on first call. With --log-level info:
[2026-05-06 ...][T0x...][INFO] pto_cpu_sim_acquire_device: cpu_sim_context.cpp:167] cpu_sim_context: acquired device 0
[2026-05-06 ...][T0x...][INFO] init_runtime_impl: runtime_maker.cpp:119] Registering 3 kernel(s) ...
Both lines carry the same HostLogger-formatted prefix (timestamp, thread
id, level tag), proving that cpu_sim_context.so and host_runtime.so
resolve to the same HostLogger instance. If singleton sharing were
broken, cpu_sim_context.so would have its own HostLogger defaulting to
TIMING and the INFO diagnostic would be silenced entirely.
Output formats¶
Host (HostLogger::emit)¶
Timestamp is local time with microsecond precision; T0x... is
pthread_self(). Both prefixes are added before the level/func segments so
parallel-test stderr from pytest-xdist is recoverable via sort -k1
(timestamp) and grep T0x... (per-thread).
AICPU sim¶
[DEBUG] func: [file.cpp:line] message
[INFO] func: [file.cpp:line] message
[TIMING] func: [file.cpp:line] message
[WARN] func: [file.cpp:line] message
[ERROR] func: [file.cpp:line] message
No timestamp/tid — the AICPU sim path is its own dev_vlog_* writing
straight to stderr. Onboard AICPU goes through CANN dlog and inherits
its format. Device TIMING uses CANN WARN and carries a [TIMING] message tag.
Configuration flow¶
| Stage | Action | Source |
|---|---|---|
| Python import | _log.py registers TIMING / NUL; sets simpler logger to TIMING if untouched |
python/simpler/_log.py |
Worker.init() |
reads and normalizes the simpler logger's effective threshold |
python/simpler/_log.py:get_current_config() |
ChipWorker.init() (Python) |
ctypes.CDLL(libsimpler_log.so, RTLD_GLOBAL) → simpler_log_init(level) (seeds HostLogger) → ctypes.CDLL(libcpu_sim_context.so, RTLD_GLOBAL) (sim) → _ChipWorker.init |
python/simpler/task_interface.py |
simpler_log_init |
HostLogger.set_level — only writer of the host filter |
src/common/log/host_log.cpp |
_ChipWorker.init() (C++) |
dlopen(host_runtime.so, RTLD_LOCAL) → dlsym → simpler_init |
src/common/worker/chip_worker.cpp |
simpler_init (per platform) |
(onboard) dlog_setlevel(HostLogger.cann_level()) before device-context open; then attach thread and transfer executor binaries |
src/common/platform/{onboard,sim}/host/c_api_shared.cpp |
| AICPU init | runner writes HostLogger.level() into InitArgs; AICPU calls set_log_level once |
src/{arch}/platform/onboard/aicpu/kernel.cpp |
The Python-side level snapshot is one-shot at Worker.init(). Calling
logger.setLevel(...) afterwards has no effect on a live ChipWorker —
recreate the worker if mid-run reconfiguration is needed.
Build orchestration¶
libsimpler_log.so is built once per pip install (not per
arch/variant) before any consumer .so. libcpu_sim_context.so follows
the same pattern (one process-global copy, sim builds only).
| Step | File | Function |
|---|---|---|
| CMake project | src/common/log/CMakeLists.txt |
add_library(simpler_log SHARED ...) |
| Compile invocation | simpler_setup/runtime_compiler.py |
RuntimeCompiler.compile_simpler_log() |
| Build / lookup wrapper | simpler_setup/runtime_builder.py |
RuntimeBuilder.ensure_simpler_log() |
| Top-level orchestration | simpler_setup/build_runtimes.py |
builds simpler_log once before the per-platform loop |
| Output path | — | build/lib/libsimpler_log.so |
| Path resolution at runtime | simpler_setup/runtime_builder.py |
RuntimeBinaries.simpler_log_path |
ChipWorker.init(device_id, bins) reads bins.simpler_log_path (and, on sim,
bins.sim_context_path) from RuntimeBinaries and ctypes.CDLL(..., mode=
RTLD_GLOBAL)s them before handing off to the C++ _ChipWorker.init.
Where to look for what¶
| You want to … | Look at |
|---|---|
| Change the user-facing single-knob model | python/simpler/_log.py + docs/testing.md § Log levels |
| Change the host output format / pattern | src/common/log/host_log.cpp::HostLogger::emit |
| Change the sim AICPU output format | src/common/platform/sim/aicpu/device_log.cpp::dev_vlog_* |
| Change the onboard AICPU CANN dlog tagging | src/common/platform/onboard/aicpu/device_log.cpp::dev_vlog_* |
| Add a new C ABI entry point (e.g. dynamic config push) | src/common/log/include/common/unified_log.h + unified_log_host.cpp + src/common/platform/shared/aicpu/unified_log_device.cpp |
Hook a new consumer .so |
declare target_include_directories(target PRIVATE src/common/log/include); for host code also link simpler_log (or use undefined symbol resolution at runtime via RTLD_GLOBAL load) |
| Add a new level | common/log_level.h + python/simpler/_log.py + simpler_setup/log_config.py + AICPU set_log_level |