<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Ssh on Lessons learned</title><link>https://eghuro.cz/tags/ssh/</link><description>Recent content in Ssh on Lessons learned</description><generator>Hugo</generator><language>en-us</language><copyright>© Alexandr Mansurov · Licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/)</copyright><lastBuildDate>Sat, 26 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://eghuro.cz/tags/ssh/index.xml" rel="self" type="application/rss+xml"/><item><title>A failed CUDA build over SSH still launched the old executable</title><link>https://eghuro.cz/posts/remote-failed-build-stale-executable/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/remote-failed-build-stale-executable/</guid><description>&lt;p&gt;A remote command returned a CUDA compilation error followed by a normal simulation startup banner:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;error: calling a __device__ function(&amp;#34;__sinf&amp;#34;) from a __host__ __device__ function(&amp;#34;inflowFluctuation&amp;#34;) is not allowed&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The simulation started, but it ran an older executable. Semicolons let the launch proceed after &lt;code&gt;nvcc&lt;/code&gt; failed. Correcting the source and rebuilding recovered the test; guarding the build-to-launch transition would prevent that misleading result.&lt;/p&gt;&#10;&lt;p&gt;The &lt;a&#10; href="https://eghuro.cz/posts/claude-code-ssh-openpbs-setup"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;Claude Code and OpenPBS setup&lt;/a&gt;&#10; explains the local-agent/remote-command boundary. This happened during an early SSH-driven test, before we moved the long-running work into scheduled jobs. The shell mistake also applies inside a PBS script.&lt;/p&gt;&#10;&#10;&lt;h2 id="diagnostics-did-not-control-execution"&gt;Diagnostics did not control execution &lt;/h2&gt;&#10;&lt;p&gt;The command had this structure, with project paths removed:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nvcc -o solver solver.cu &amp;gt; build.log 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;grep -A3 error build.log &lt;span class="p"&gt;|&lt;/span&gt; head&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nohup ./solver &lt;span class="k"&gt;case&lt;/span&gt;.cfg &amp;gt; run.log 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt; &amp;lt; /dev/null &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The build wrote to an executable path that already existed. Compilation failed, leaving a runnable earlier build. Printing matching diagnostics didn&amp;rsquo;t stop the next command, and backgrounding the program didn&amp;rsquo;t establish whether the requested source had compiled.&lt;/p&gt;&#10;&lt;p&gt;The failed-build result included an ordinary banner. After replacing &lt;code&gt;__sinf&lt;/code&gt; with &lt;code&gt;sinf&lt;/code&gt; in the host/device function, the rebuilt program&amp;rsquo;s banner contained the newly added &lt;code&gt;inflowTurb=0.08&lt;/code&gt; parameter and subsequent progress lines. The earlier banner lacked that field. That difference provided evidence of which code path actually ran.&lt;/p&gt;&#10;&lt;p&gt;The transcript shows this operational recovery. Its retry still used semicolon-separated build and launch commands, so it did not repair the fail-open shell structure.&lt;/p&gt;&#10;&#10;&lt;h2 id="make-compiler-status-a-launch-prerequisite"&gt;Make compiler status a launch prerequisite &lt;/h2&gt;&#10;&lt;p&gt;For a future build, preserve the compiler&amp;rsquo;s status explicitly. This generalized Bash pattern builds to a temporary path and only publishes it after success:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_DIR&lt;/span&gt;&lt;span class="p"&gt;:?Set PROJECT_DIR to the remote project directory&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;candidate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp ./solver.build.XXXXXX&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rm -f &amp;#34;$candidate&amp;#34;&amp;#39;&lt;/span&gt; EXIT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; nvcc -o &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; solver.cu &amp;gt; build.log 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; mv -f &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$candidate&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; ./solver&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;else&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; cat build.log &amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;exit&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;fi&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; ./solver &lt;span class="k"&gt;case&lt;/span&gt;.cfg&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is a recommended guard, not code installed during the incident. The named source and configuration must exist in the remote project. Use a single build owner: concurrent publishers to &lt;code&gt;./solver&lt;/code&gt; require a separate ownership or versioned-artifact design.&lt;/p&gt;&#10;&lt;p&gt;When running remotely, send the whole script through SSH so its directory selection and status checks happen in the same remote shell. Avoid embedding local path expansions accidentally. For batch work, run the payload in the allocated job rather than adding another unscheduled &lt;code&gt;nohup&lt;/code&gt; launch.&lt;/p&gt;&#10;&#10;&lt;h2 id="reproduce-the-shell-failure-without-cuda"&gt;Reproduce the shell failure without CUDA &lt;/h2&gt;&#10;&lt;p&gt;On a local machine with a POSIX shell, this harmless fixture leaves an existing command available after a simulated build failure:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh -c &lt;span class="s1"&gt;&amp;#39;solver() { printf &amp;#34;old executable ran\n&amp;#34;; }; false; solver&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It prints &lt;code&gt;old executable ran&lt;/code&gt; and exits successfully. The guarded form prevents execution:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sh -c &lt;span class="s1"&gt;&amp;#39;solver() { printf &amp;#34;old executable ran\n&amp;#34;; }; false &amp;amp;&amp;amp; solver&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That form prints nothing and returns a failure status. For a real compiler test, deliberately introduce an error and verify that no new run starts. Also log a build identifier or other deliberate runtime identity; a healthy-looking startup line only proves that some executable ran.&lt;/p&gt;&#10;&lt;p&gt;Check your next remote build command for the exact status dependency between compilation and launch, especially where diagnostic pipelines sit between them.&lt;/p&gt;&#10;</description></item><item><title>A second launch of a resumable worker: 'OSError: [Errno 39] Directory not empty'</title><link>https://eghuro.cz/posts/resumable-worker-concurrent-launch/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/resumable-worker-concurrent-launch/</guid><description>&lt;p&gt;A resumable remote dataset driver failed while cleaning an unfinished task directory:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;OSError: [Errno 39] Directory not empty&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Another worker was still writing there. The second launch treated an unfinished task as safe to restart, even though the first process already owned it. Checking the surviving worker and validating its affected output recovered that work unit; no locking fix was installed during the session.&lt;/p&gt;&#10;&lt;p&gt;This was an early detached-run failure in the &lt;a&#10; href="https://eghuro.cz/posts/claude-code-ssh-openpbs-setup"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;Claude Code, SSH, and OpenPBS workflow&lt;/a&gt;&#10;. Scheduler-managed execution later improved job tracking, but an application still needs to prevent conflicting owners.&lt;/p&gt;&#10;&#10;&lt;h2 id="the-waiting-wrapper-had-not-disappeared"&gt;The waiting wrapper had not disappeared &lt;/h2&gt;&#10;&lt;p&gt;The first launch created a detached wrapper that waited for a pilot, then became the production driver. Its structure was:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;until&lt;/span&gt; grep -q &lt;span class="s2"&gt;&amp;#34;finished&amp;#34;&lt;/span&gt; pilot.log&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; sleep 30&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; python -u run_dataset.py dataset &amp;gt;&amp;gt; production.log 2&amp;gt;&lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Before the transition, a search for the production process would not find that driver. After &lt;code&gt;exec&lt;/code&gt;, a search for the wrapper&amp;rsquo;s old command name would no longer identify it. Neither snapshot alone described the complete lifecycle.&lt;/p&gt;&#10;&lt;p&gt;A later check did not establish that the waiting launch was gone. Another detached production command started. The resulting log contained two identical pending-work counts followed by a traceback from &lt;code&gt;shutil.rmtree(gdir)&lt;/code&gt;. Both drivers had selected the same unfinished task, and one attempted cleanup while the other&amp;rsquo;s solver wrote files.&lt;/p&gt;&#10;&lt;p&gt;The resume logic used completion markers to skip finished groups. It offered no exclusive claim on an unfinished directory.&lt;/p&gt;&#10;&#10;&lt;h2 id="recovery-required-checking-the-survivor"&gt;Recovery required checking the survivor &lt;/h2&gt;&#10;&lt;p&gt;The follow-up inspected running processes, restored the affected task&amp;rsquo;s configuration, and checked the surviving computation. Its original log file had been lost during cleanup. A later driver result reported that the task was accepted.&lt;/p&gt;&#10;&lt;p&gt;A broad grep for success also matched text in the traceback. Tightening the check to a result-line pattern distinguished the actual accepted task from an error message that happened to contain matching characters.&lt;/p&gt;&#10;&lt;p&gt;That evidence supports recovery of the affected work unit. It does not prove that no intermediate data was lost or that the session did not add a lock. Eventual production completion checked in a later session also cannot establish that overlapping launches became safe.&lt;/p&gt;&#10;&#10;&lt;h2 id="claim-ownership-before-destructive-resume"&gt;Claim ownership before destructive resume &lt;/h2&gt;&#10;&lt;p&gt;For a single-host driver using a filesystem with working advisory locks, a whole-run lock is one possible improvement. On a system with util-linux &lt;code&gt;flock&lt;/code&gt;, run from the project directory:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;flock -n .production.lock python -u run_dataset.py dataset&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Every launcher must use the same stable lock path. Do not delete and recreate the lock file while work is running. The payload named here is the application&amp;rsquo;s driver, not a standard tool.&lt;/p&gt;&#10;&lt;p&gt;This guard is a new recommendation. A small local check can verify contention without running a dataset:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;fcntl&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;tempfile&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryDirectory&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;directory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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="n"&gt;directory&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/worker.lock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;w&amp;#34;&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;first&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_EX&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_NB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&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="n"&gt;directory&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/worker.lock&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;w&amp;#34;&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;second&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;second&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_EX&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LOCK_NB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;BlockingIOError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;second owner rejected&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;overlapping owner accepted&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Cluster filesystems and multi-node workers need an ownership mechanism tested under their actual locking semantics. Per-task claims can allow parallelism that a whole-run lock forbids. &lt;a&#10; href="https://eghuro.cz/posts/openpbs-resume-stable-shard-ids"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;Stable PBS shard assignment&lt;/a&gt;&#10; solves a different problem: preserving each worker&amp;rsquo;s task set across starts and restarts. It cannot exclude two copies of the same shard.&lt;/p&gt;&#10;&lt;p&gt;Before retrying unfinished work, establish whether a live process already owns it. A missing completion marker only says that completion has not been recorded.&lt;/p&gt;&#10;</description></item><item><title>Claude Code over SSH: submit long-running work to OpenPBS</title><link>https://eghuro.cz/posts/claude-code-ssh-openpbs-setup/</link><pubDate>Sat, 26 Sep 2026 00:00:00 +0000</pubDate><guid>https://eghuro.cz/posts/claude-code-ssh-openpbs-setup/</guid><description>&lt;p&gt;Claude Code can prepare a computation on your laptop and operate a remote cluster through ordinary SSH commands. In this setup, Claude Code ran locally on macOS; source uploads, compilation, queue submissions, and checks ran on a Linux server. OpenPBS handled the long-running computation after submission, so the laptop didn&amp;rsquo;t need to keep an SSH session open.&lt;/p&gt;&#10;&lt;p&gt;The operating rule was explicit: work estimated at one hour or more belonged in PBS. That threshold was a local policy, not an OpenPBS default. Use your site&amp;rsquo;s rules, including its restrictions on short tests and compilation on login nodes.&lt;/p&gt;&#10;&#10;&lt;h2 id="establish-the-ssh-connection-yourself"&gt;Establish the SSH connection yourself &lt;/h2&gt;&#10;&lt;p&gt;Configure authentication and verify the server&amp;rsquo;s host key before asking an agent to connect. For example, in your local &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code class="language-sshconfig" data-lang="sshconfig"&gt;Host compute-login&#10; HostName 192.168.0.2&#10; User researcher&#10; BatchMode yes&#10; ConnectTimeout 10&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The address and account are placeholders. With your site&amp;rsquo;s actual values and authentication configured, run this from the laptop:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh compute-login &lt;span class="s1"&gt;&amp;#39;hostname; pwd; command -v qsub; command -v python3&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;BatchMode&lt;/code&gt; makes missing authentication fail instead of waiting for a password. It does not approve the connection or replace host-key verification. Give Claude permission for the intended remote actions through your normal tool-approval workflow; never paste a private key into the conversation.&lt;/p&gt;&#10;&#10;&lt;h2 id="make-the-execution-boundary-explicit"&gt;Make the execution boundary explicit &lt;/h2&gt;&#10;&lt;p&gt;A useful project instruction is:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Claude runs on the laptop. Use SSH for remote operations.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Keep this project&amp;#39;s remote files in ~/batch-demo.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Submit tasks estimated at one hour or more through OpenPBS.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Follow site policy for shorter tests; do not bypass the scheduler.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Record the job ID after submission. Check logs and expected outputs before reporting completion. Ask before cancellation or deletion.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Save this with the project&amp;rsquo;s agent instructions. A prose rule still needs checking: our early sessions launched substantial work directly before the PBS rule was captured. Also, having a GPU on a login host does not grant permission to use it.&lt;/p&gt;&#10;&#10;&lt;h2 id="submit-a-small-smoke-test-first"&gt;Submit a small smoke test first &lt;/h2&gt;&#10;&lt;p&gt;These are generalized setup examples, not a copy of the original scientific workload. Create &lt;code&gt;run.py&lt;/code&gt; locally:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;socket&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;sys&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&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;&amp;#34;worker host: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gethostname&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&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;&amp;#34;python: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;smoke test complete&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Create &lt;code&gt;run.pbs&lt;/code&gt; beside it:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#!/bin/bash&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -N agent-smoke&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -l select=1:ncpus=1&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -l walltime=00:05:00&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;#PBS -j oe&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; -euo pipefail&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PBS_O_WORKDIR&lt;/span&gt;&lt;span class="p"&gt;:?PBS submission directory missing&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;/.venv/bin/python&amp;#34;&lt;/span&gt; -u run.py&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;From the laptop, prepare and submit it:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh compute-login &lt;span class="s1"&gt;&amp;#39;mkdir -p ~/batch-demo &amp;amp;&amp;amp; cd ~/batch-demo &amp;amp;&amp;amp; python3 -m venv .venv&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scp run.py run.pbs compute-login:batch-demo/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ssh compute-login &lt;span class="s1"&gt;&amp;#39;cd ~/batch-demo &amp;amp;&amp;amp; qsub run.pbs&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Record the exact job ID that &lt;code&gt;qsub&lt;/code&gt; returns. The batch script selects its directory and interpreter without depending on an activated interactive shell. Install real dependencies according to site policy and check that the environment and project directory are accessible on the execution node. Shared paths and compatible Python installations are prerequisites, not promises made by SSH.&lt;/p&gt;&#10;&lt;p&gt;For GPU work, request the site&amp;rsquo;s documented GPU resources. The original array used &lt;code&gt;select=1:ncpus=2:ngpus=1&lt;/code&gt; per element. GPU visibility variables and binding depend on cluster configuration; inspect them inside an allocation rather than choosing an arbitrary device from the login host.&lt;/p&gt;&#10;&#10;&lt;h2 id="follow-the-job-through-its-outputs"&gt;Follow the job through its outputs &lt;/h2&gt;&#10;&lt;p&gt;On the login host, use &lt;code&gt;qstat -f&lt;/code&gt; with the recorded ID for detailed status. Read the output file named in that status. Sites retaining finished-job history may also support &lt;code&gt;qstat -xf&lt;/code&gt; for accounting. The &lt;a&#10; href="https://github.com/openpbs/openpbs/blob/master/doc/man1/qsub.1B"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;upstream qsub manual&lt;/a&gt;&#10; documents submission options and &lt;code&gt;PBS_O_WORKDIR&lt;/code&gt;; the &lt;a&#10; href="https://github.com/openpbs/openpbs/blob/master/doc/man1/qstat.1B"&#10; &#10; target="_blank" rel="noopener"&#10; &#10; &#10;&gt;qstat manual&lt;/a&gt;&#10; covers status queries.&lt;/p&gt;&#10;&lt;p&gt;An empty queue is insufficient evidence of success. For this smoke test, check the reported worker and Python path and the completion line. For production, also verify expected task outcomes and output contents.&lt;/p&gt;&#10;&lt;p&gt;The subsequent failures were about execution ownership and verification:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/remote-failed-build-stale-executable"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;A failed remote build launched an old executable&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/resumable-worker-concurrent-launch"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;A second detached launch collided with resumable work&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/openpbs-resume-stable-shard-ids"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;PBS array resume needed stable task IDs&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;li&gt;&lt;a&#10; href="https://eghuro.cz/posts/rsync-atomic-archives-incomplete-release"&#10; &#10; data-rel="prefetch"&#10; &#10; &#10;&gt;Archive transfer started before the release was complete&lt;/a&gt;&#10;.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Before handing over a long run, submit the smoke test and check its output on the execution node&amp;rsquo;s actual environment.&lt;/p&gt;&#10;</description></item></channel></rss>