From 4499eaa53ad3abea7ac9841c5b9e173bf5216e51 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:15:12 +1100 Subject: [PATCH 1/2] linuxcnc: default OMP_NUM_THREADS to 1 An OpenMP build of OpenBLAS reserves 128 MiB buffers at load, about 1 GiB of private writable memory per CPU. The preview imports numpy, which loads it, so every GUI with a preview holds that reservation. With the default overcommit mode a fork() must account it again, and on a machine with less RAM than that the kernel refuses: __vm_enough_memory: pid: 2586, comm: axis, bytes: 10737418240 not enough memory for the allocation OSError: [Errno 12] Cannot allocate memory The GUIs that fork after the import fail to start: axis, gmoccapy and qtvcp for the POSTGUI_HALFILE, and qtdragon also in QtWebEngine. Reported by NTULINUX. OMP_NUM_THREADS=1 drops the reservation (8.5 GB to 1.2 GB VmSize at 8 CPUs). OPENBLAS_NUM_THREADS=1 does not. LinuxCNC does no BLAS work that needs threads. A value the user set is kept. --- scripts/linuxcnc.in | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/linuxcnc.in b/scripts/linuxcnc.in index 3ab645da498..9e6de5efe1a 100644 --- a/scripts/linuxcnc.in +++ b/scripts/linuxcnc.in @@ -33,6 +33,11 @@ if [[ ! -v LINUXCNC_OPENGL_PLATFORM || "${LINUXCNC_OPENGL_PLATFORM}" == "glx" ]] export QT_QPA_PLATFORM="xcb" fi +# An OpenMP build of OpenBLAS reserves about 1 GiB per CPU when numpy +# is imported; with less RAM than that the kernel refuses to fork the +# GUI (ENOMEM). Nothing here needs threaded BLAS. +export OMP_NUM_THREADS="${OMP_NUM_THREADS:-1}" + ################################################################################ # 0. Values that come from configure ################################################################################ From d53b4d9044627d042d7740f42b56867991ddc242 Mon Sep 17 00:00:00 2001 From: Luca Toniolo <10792599+grandixximo@users.noreply.github.com> Date: Wed, 28 Oct 2026 19:15:12 +1100 Subject: [PATCH 2/2] gui: run the postgui HAL files without forking the GUI axis, gmoccapy, gscreen and qtvcp ran POSTGUI_HALFILE and POSTGUI_HALCMD through os.spawnvp(), which forks the whole GUI process. A fork must account the GUI's private writable memory again, and with the preview's numpy that can be gigabytes of reservation (see the OMP_NUM_THREADS commit). subprocess.call() starts the child with vfork/posix_spawn, which shares the parent's memory until exec and accounts nothing. The return value is the same: the exit status, or minus the signal. gmoccapy's comment on SIGTERM lost in a fork hook no longer points at the postgui spawns; the check stays for any other fork. --- src/emc/usr_intf/axis/scripts/axis.py | 8 ++++---- src/emc/usr_intf/gmoccapy/gmoccapy.py | 8 ++++---- src/emc/usr_intf/gscreen/gscreen.py | 4 ++-- src/emc/usr_intf/qtvcp/qtvcp.py | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/emc/usr_intf/axis/scripts/axis.py b/src/emc/usr_intf/axis/scripts/axis.py index 9477cca0332..1c00b3db1e6 100755 --- a/src/emc/usr_intf/axis/scripts/axis.py +++ b/src/emc/usr_intf/axis/scripts/axis.py @@ -19,7 +19,7 @@ # import pdb -import sys, os +import sys, os, subprocess import string BASE = os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]), "..")) sys.path.insert(0, os.path.join(BASE, "lib", "python")) @@ -4298,15 +4298,15 @@ def check_dynamic_tabs(): if postgui_halfile is not None: for f in postgui_halfile: if f.lower().endswith('.tcl'): - res = os.spawnvp(os.P_WAIT, "haltcl", ["haltcl", "-i", vars.emcini.get(), f]) + res = subprocess.call(["haltcl", "-i", vars.emcini.get(), f]) else: - res = os.spawnvp(os.P_WAIT, "halcmd", ["halcmd", "-i", vars.emcini.get(), "-f", f]) + res = subprocess.call(["halcmd", "-i", vars.emcini.get(), "-f", f]) if res: raise SystemExit(res) if postgui_halcmds is not None: for f in postgui_halcmds: f = os.path.expanduser(f) - res = os.spawnvp(os.P_WAIT, "halcmd", ["halcmd"] + f.split()) + res = subprocess.call(["halcmd"] + f.split()) if res: raise SystemExit(res) open_initial_file() diff --git a/src/emc/usr_intf/gmoccapy/gmoccapy.py b/src/emc/usr_intf/gmoccapy/gmoccapy.py index 4d448c43fb8..cf17081c331 100644 --- a/src/emc/usr_intf/gmoccapy/gmoccapy.py +++ b/src/emc/usr_intf/gmoccapy/gmoccapy.py @@ -6678,9 +6678,9 @@ def _make_hal_pins(self): if postgui_halfile is not None: for f in postgui_halfile: if f.lower().endswith('.tcl'): - res = os.spawnvp(os.P_WAIT, "haltcl", ["haltcl", "-i", inifile, f]) + res = subprocess.call(["haltcl", "-i", inifile, f]) else: - res = os.spawnvp(os.P_WAIT, "halcmd", ["halcmd", "-i", inifile, "-f", f]) + res = subprocess.call(["halcmd", "-i", inifile, "-f", f]) if res: LOG.error('postgui halfile error:{}'.format(res)) raise SystemExit(res) @@ -6690,7 +6690,7 @@ def _make_hal_pins(self): if postgui_halcmds is not None: for f in postgui_halcmds: f = os.path.expanduser(f) - res = os.spawnvp(os.P_WAIT, "halcmd", ["halcmd"] + f.split()) + res = subprocess.call(["halcmd"] + f.split()) if res: LOG.error('postgui command error:{}'.format(res)) raise SystemExit(res) @@ -6718,7 +6718,7 @@ def _terminate(signum, frame): signal.signal(signal.SIGINT, _terminate) # The startup handler's SystemExit is lost where Python ignores - # exceptions, as in the os.fork() hooks of the postgui spawns above. + # exceptions, such as in an os.fork() hook. # The flag still says that a SIGTERM came. if _terminate_requested: LOG.info("gmoccapy received SIGTERM during startup, shutting down") diff --git a/src/emc/usr_intf/gscreen/gscreen.py b/src/emc/usr_intf/gscreen/gscreen.py index 6a89904ca69..491e86a7f74 100755 --- a/src/emc/usr_intf/gscreen/gscreen.py +++ b/src/emc/usr_intf/gscreen/gscreen.py @@ -4797,9 +4797,9 @@ def __setitem__(self, item, value): if postgui_halfile is not None: for f in postgui_halfile: if f.lower().endswith('.tcl'): - res = os.spawnvp(os.P_WAIT, "haltcl", ["haltcl", "-i", inifile, f]) + res = subprocess.call(["haltcl", "-i", inifile, f]) else: - res = os.spawnvp(os.P_WAIT, "halcmd", ["halcmd", "-i", inifile, "-f", f]) + res = subprocess.call(["halcmd", "-i", inifile, "-f", f]) if res: raise SystemExit(res) Gtk.main() diff --git a/src/emc/usr_intf/qtvcp/qtvcp.py b/src/emc/usr_intf/qtvcp/qtvcp.py index 844631ac297..e4f5166fb7a 100644 --- a/src/emc/usr_intf/qtvcp/qtvcp.py +++ b/src/emc/usr_intf/qtvcp/qtvcp.py @@ -499,9 +499,9 @@ def postgui(self): for f in postgui_halfile: f = os.path.expanduser(f) if f.lower().endswith('.tcl'): - res = os.spawnvp(os.P_WAIT, "haltcl", ["haltcl", "-i",self.inipath, f]) + res = subprocess.call(["haltcl", "-i",self.inipath, f]) else: - res = os.spawnvp(os.P_WAIT, "halcmd", ["halcmd", "-i",self.inipath,"-f", f]) + res = subprocess.call(["halcmd", "-i",self.inipath,"-f", f]) if res: raise SystemExit(res) def postgui_cmd(self): @@ -510,7 +510,7 @@ def postgui_cmd(self): if postgui_commands is not None: for f in postgui_commands: f = os.path.expanduser(f) - res = os.spawnvp(os.P_WAIT, "halcmd", ["halcmd"] + f.split()) + res = subprocess.call(["halcmd"] + f.split()) if res: raise SystemExit(res) # This can be called by control c or an early error.