<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://benjijang.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://benjijang.com/" rel="alternate" type="text/html" /><updated>2026-03-28T18:58:39-07:00</updated><id>https://benjijang.com/feed.xml</id><title type="html">Benjamin Steenhoek</title><subtitle>Senior Researcher at Microsoft</subtitle><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><entry><title type="html">Windows Terminal profile for Claude with no permission prompts</title><link href="https://benjijang.com/posts/2026/03/claude-terminal-profile/" rel="alternate" type="text/html" title="Windows Terminal profile for Claude with no permission prompts" /><published>2026-03-28T00:00:00-07:00</published><updated>2026-03-28T00:00:00-07:00</updated><id>https://benjijang.com/posts/2026/03/claude-terminal-profile</id><content type="html" xml:base="https://benjijang.com/posts/2026/03/claude-terminal-profile/"><![CDATA[<p>Every time I open Claude Code in a new directory I get a workspace trust prompt asking me to approve permissions. It makes sense as a safety guardrail, but in directories I own and trust it’s just friction. I made a dedicated Windows Terminal profile that launches Claude with <code class="language-plaintext highlighter-rouge">--permission-mode bypassPermissions</code> so it skips straight to work.</p>

<p>Here’s the gist: <a href="https://gist.github.com/bstee615/9b43dd9e40f91f0f082f5697968f04b3">Windows Terminal profile for Claude</a>.</p>

<p>Here’s what it looks like:</p>

<p><img src="/images/screenshot-claude-terminal.png" alt="Screenshot of Claude terminal quick launcher" /></p>

<p>You can install it with a PowerShell one-liner:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">irm</span><span class="w"> </span><span class="s1">'https://gist.githubusercontent.com/bstee615/9b43dd9e40f91f0f082f5697968f04b3/raw/setup.ps1'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">iex</span><span class="w">
</span></code></pre></div></div>

<p>Or add this manually to <code class="language-plaintext highlighter-rouge">profiles.list</code> in your Windows Terminal <code class="language-plaintext highlighter-rouge">settings.json</code> (<code class="language-plaintext highlighter-rouge">Ctrl+,</code> → <strong>Open JSON file</strong>):</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"commandline"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cmd.exe /c claude --permission-mode bypassPermissions"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"guid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{a1b2c3d4-e5f6-7890-abcd-ef1234567890}"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"hidden"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Claude"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"startingDirectory"</span><span class="p">:</span><span class="w"> </span><span class="s2">"%USERPROFILE%"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The setup script checks for an existing profile by GUID before inserting, so running it twice is safe. One caveat: only use this flag in directories you own or fully trust — it disables all permission checks.</p>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="blog-post" /><category term="tooling" /><summary type="html"><![CDATA[Every time I open Claude Code in a new directory I get a workspace trust prompt asking me to approve permissions. It makes sense as a safety guardrail, but in directories I own and trust it’s just friction. I made a dedicated Windows Terminal profile that launches Claude with --permission-mode bypassPermissions so it skips straight to work.]]></summary></entry><entry><title type="html">Tailscale + SSH setup for Windows + WSL2</title><link href="https://benjijang.com/posts/2026/03/tailscale-wsl2-ssh/" rel="alternate" type="text/html" title="Tailscale + SSH setup for Windows + WSL2" /><published>2026-03-28T00:00:00-07:00</published><updated>2026-03-28T00:00:00-07:00</updated><id>https://benjijang.com/posts/2026/03/tailscale-wsl2-ssh</id><content type="html" xml:base="https://benjijang.com/posts/2026/03/tailscale-wsl2-ssh/"><![CDATA[<p>I do a lot of work from my phone while away from my desk and wanted a clean way to SSH into my Windows machine and drop into whichever WSL2 distro I needed. The tricky part: iOS Tailscale intercepts port 22, WSL2 distros share a network namespace so you can’t run independent sshd instances on the same port, and adding a Tailscale node per distro causes TUN device conflicts.</p>

<p>The solution I landed on was running a single Windows OpenSSH server on three ports — 2222 for PowerShell, 2223 for Debian, 2224 for Alpine — and using <code class="language-plaintext highlighter-rouge">Match LocalPort</code> directives in <code class="language-plaintext highlighter-rouge">sshd_config</code> to forward each port to the right WSL distro via <code class="language-plaintext highlighter-rouge">wsl.exe</code>. One Tailscale node on Windows handles all the routing.</p>

<h1 id="tldr">TL;DR</h1>

<p>See the gist: <a href="https://gist.github.com/bstee615/d510992d535e955fb175028eb5c5c4d0">Tailscale + SSH setup for Windows + WSL2</a>.</p>

<p>The key bit in <code class="language-plaintext highlighter-rouge">sshd_config</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Port 2222
Port 2223
Port 2224

Match LocalPort 2223
       ForceCommand C:\Windows\System32\wsl.exe -d Debian --cd ~

Match LocalPort 2224
       ForceCommand C:\Windows\System32\wsl.exe -d Alpine --cd ~
</code></pre></div></div>

<p>Then connect with:</p>

<table>
  <thead>
    <tr>
      <th>Command</th>
      <th>Destination</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ssh -p 2222 myuser@zap</code></td>
      <td>Windows PowerShell</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ssh -p 2223 myuser@zap</code></td>
      <td>Debian bash</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">ssh -p 2224 myuser@zap</code></td>
      <td>Alpine sh</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="blog-post" /><category term="tooling" /><summary type="html"><![CDATA[I do a lot of work from my phone while away from my desk and wanted a clean way to SSH into my Windows machine and drop into whichever WSL2 distro I needed. The tricky part: iOS Tailscale intercepts port 22, WSL2 distros share a network namespace so you can’t run independent sshd instances on the same port, and adding a Tailscale node per distro causes TUN device conflicts.]]></summary></entry><entry><title type="html">How to limit the number of Twitter posts in your timeline using JavaScript</title><link href="https://benjijang.com/posts/2024/03/twitter-limiter/" rel="alternate" type="text/html" title="How to limit the number of Twitter posts in your timeline using JavaScript" /><published>2024-03-12T00:00:00-07:00</published><updated>2024-03-12T00:00:00-07:00</updated><id>https://benjijang.com/posts/2024/03/twitter-limiter</id><content type="html" xml:base="https://benjijang.com/posts/2024/03/twitter-limiter/"><![CDATA[<p>I have a bit of a problem scrolling too much of the awesome content on Twitter. There are just too many other people creating cool ideas and software! At first I justified it by the fact that I find academic papers (my feed is curated to mostly academic CS/ML/SE), but now I admit it’s too much!
In a bid to waste more time in order to waste less time, I created a <a href="https://gist.github.com/bstee615/1f4aa8a6d63dc74aff53dac17d287d91">simple script</a> that puts an extra UI onto each post in my Twitter feed:</p>
<ul>
  <li>For the first five posts, it numbers them to remind me how many I’ve browsed.</li>
  <li>After that, it blocks them out with a reminder of the limit I set (currently I keep it at five posts).</li>
</ul>

<p>Here’s what the result looks like:
<img src="/files/twitter_limiter_demo.png" alt="A screenshot of twitter.com showing the post numbering and limiting features." /></p>

<p>Here’s the link to the script: <a href="https://gist.github.com/bstee615/1f4aa8a6d63dc74aff53dac17d287d91">JavaScript for Twitter Post Limiter · GitHub</a>.
I currently import it automatically with <a href="https://violentmonkey.github.io/">ViolentMonkey</a>.
Plans are to turn it into a FireFox extension (once conference deadline season is over 🙃).
Hope it’s useful!</p>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="blog-post" /><category term="fun" /><category term="tooling" /><summary type="html"><![CDATA[I have a bit of a problem scrolling too much of the awesome content on Twitter. There are just too many other people creating cool ideas and software! At first I justified it by the fact that I find academic papers (my feed is curated to mostly academic CS/ML/SE), but now I admit it’s too much! In a bid to waste more time in order to waste less time, I created a simple script that puts an extra UI onto each post in my Twitter feed: For the first five posts, it numbers them to remind me how many I’ve browsed. After that, it blocks them out with a reminder of the limit I set (currently I keep it at five posts).]]></summary></entry><entry><title type="html">How to set up a shared cache for HuggingFace libraries</title><link href="https://benjijang.com/posts/2024/01/shared-hf-cache/" rel="alternate" type="text/html" title="How to set up a shared cache for HuggingFace libraries" /><published>2024-01-16T00:00:00-08:00</published><updated>2024-01-16T00:00:00-08:00</updated><id>https://benjijang.com/posts/2024/01/shared-huggingface-cache</id><content type="html" xml:base="https://benjijang.com/posts/2024/01/shared-hf-cache/"><![CDATA[<h1 id="tldr">TL;DR</h1>
<p>I set up a shared cache for HuggingFace libraries like <code class="language-plaintext highlighter-rouge">transformers</code> and <code class="language-plaintext highlighter-rouge">datasets</code>.
See the repository: <a href="https://github.com/bstee615/shared-hf-cache">https://github.com/bstee615/shared-hf-cache</a>.
To use it, create a shared directory which can be edited by all interested users and set the environment variable <code class="language-plaintext highlighter-rouge">export HF_HOME="/huggingface"</code>.</p>

<h1 id="the-problem-duplicated-model-checkpoints-and-datasets">The problem: duplicated model checkpoints and datasets</h1>

<p>My lab members and I use a shared machine to run, among other things, large language model inference using the <code class="language-plaintext highlighter-rouge">transformers</code> and <code class="language-plaintext highlighter-rouge">datasets</code> libraries. HuggingFace libraries download the model weights or datasets, and the downloaded files can be very large (over 50GB).
By default, the weights and datasets are downloaded to some folders under <code class="language-plaintext highlighter-rouge">~/.cache/huggingface/</code>. Different users will download copies of the same models. This causes the storage requirements to grow much larger than what is needed.</p>

<h1 id="how-to-set-up-a-shared-hf-cache">How to set up a shared HF cache</h1>

<p>In order to cut down the amount of storage used, I set up a shared directory <code class="language-plaintext highlighter-rouge">/huggingface</code> so users can all use the same folder to download their models and datasets. Users need only to set an environment variable using <code class="language-plaintext highlighter-rouge">export HF_HOME="/huggingface"</code>, then the HF libraries will download all files to the shared folder.</p>

<p>Here’s the script I used:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>

<span class="c"># Create a group for permissions to the directory</span>
<span class="nb">sudo </span>groupadd hf-users
<span class="nb">sudo </span>usermod <span class="nt">-aG</span> hf-users <span class="nv">$USER</span>

<span class="c"># Create shared directory and make it owned by the group</span>
<span class="nb">sudo mkdir</span> <span class="nt">--mode</span><span class="o">=</span>u+rwx,g+rwxs,o-rwx /huggingface <span class="c"># Give the directory rwx for user and group, and make files the directory inherit these permissions</span>
<span class="nb">sudo chown</span> <span class="nv">$USER</span> /huggingface/
<span class="nb">sudo chgrp </span>hf-users /huggingface/

<span class="c"># Add to .bashrc</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="no">EOF</span><span class="sh"> &gt;&gt; </span><span class="nv">$HOME</span><span class="sh">/.bashrc
export HF_HOME="/huggingface" # Download HF cache items to /huggingface
umask 002 # Give user and group rw/rwx by deefault
</span><span class="no">EOF

</span><span class="c"># Optional: join the group in this shell, or restart the shell</span>
newgrp hf-users
</code></pre></div></div>

<h1 id="testing-it-out">Testing it out</h1>

<p>The shared cache must be activated with these two commands. The script above adds these to your profile in ~/.bashrc.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">HF_HOME</span><span class="o">=</span><span class="s2">"/huggingface"</span> <span class="c"># Download HF cache items to /huggingface</span>
<span class="nb">umask </span>002 <span class="c"># Give user and group rw/rwx by default</span>
</code></pre></div></div>

<p>Here’s the script I used to test it out, running the same script on two different users at the same time:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Dependencies:
# pip install transformers torch accelerate bitsandbytes
</span>
<span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoModelForCausalLM</span>
<span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoTokenizer</span>

<span class="c1"># Do inference with Mistral-7B. Load the model weights ten times in a row to simulate loading the weights at the same time as another user.
</span><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Loading model..."</span><span class="p">)</span>
    <span class="n">model</span> <span class="o">=</span> <span class="n">AutoModelForCausalLM</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s">"mistralai/Mistral-7B-v0.1"</span><span class="p">,</span> <span class="n">device_map</span><span class="o">=</span><span class="s">"auto"</span><span class="p">,</span> <span class="n">load_in_4bit</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">tokenizer</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="s">"mistralai/Mistral-7B-v0.1"</span><span class="p">,</span> <span class="n">padding_side</span><span class="o">=</span><span class="s">"left"</span><span class="p">)</span>
    <span class="n">model_inputs</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">([</span><span class="s">"A list of colors: red, blue"</span><span class="p">],</span> <span class="n">return_tensors</span><span class="o">=</span><span class="s">"pt"</span><span class="p">).</span><span class="n">to</span><span class="p">(</span><span class="s">"cuda"</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Generating tokens..."</span><span class="p">)</span>
    <span class="n">generated_ids</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">generate</span><span class="p">(</span><span class="o">**</span><span class="n">model_inputs</span><span class="p">,</span> <span class="n">do_sample</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">temperature</span><span class="o">=</span><span class="mf">1.0</span><span class="p">,</span> <span class="n">max_new_tokens</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">tokenizer</span><span class="p">.</span><span class="n">batch_decode</span><span class="p">(</span><span class="n">generated_ids</span><span class="p">,</span> <span class="n">skip_special_tokens</span><span class="o">=</span><span class="bp">True</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span>
</code></pre></div></div>

<h1 id="concurrency">Concurrency</h1>

<p>The libraries include a file locking system to prevent concurrency issues from multiple users using the same files at the same time. When I tested it by trying to load the checkpoint with user B while downloading the checkpoint with user A, this gave me an error on user B like so, without interrupting user A:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PermissionError: <span class="o">[</span>Errno 13] Permission denied: <span class="s1">'/huggingface/hub/.locks/models--mistralai--Mistral-7B-v0.1/9742cb4764964155b7a5f35eefad651f590006091ddeb536863d6c5865cca1b9.lock
</span></code></pre></div></div>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="blog-post" /><category term="tooling" /><summary type="html"><![CDATA[TL;DR I set up a shared cache for HuggingFace libraries like transformers and datasets. See the repository: https://github.com/bstee615/shared-hf-cache. To use it, create a shared directory which can be edited by all interested users and set the environment variable export HF_HOME="/huggingface".]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://benjijang.com/files/unsplash/duy-pham-Cecb0_8Hx-o-unsplash.jpg" /><media:content medium="image" url="https://benjijang.com/files/unsplash/duy-pham-Cecb0_8Hx-o-unsplash.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to fix `docker-compose: command not found` error with newer versions of Docker</title><link href="https://benjijang.com/posts/2023/12/docker-compose/" rel="alternate" type="text/html" title="How to fix `docker-compose: command not found` error with newer versions of Docker" /><published>2023-12-20T00:00:00-08:00</published><updated>2023-12-20T00:00:00-08:00</updated><id>https://benjijang.com/posts/2023/12/docker-compose</id><content type="html" xml:base="https://benjijang.com/posts/2023/12/docker-compose/"><![CDATA[<p>The <code class="language-plaintext highlighter-rouge">docker-compose</code> command is missing from recent versions of Docker, replaced by a plugin built into Docker: <code class="language-plaintext highlighter-rouge">docker compose</code>.
To restore compatibility with scripts which use <code class="language-plaintext highlighter-rouge">docker-compose</code>, we can create a wrapper script which forwards its arguments to <code class="language-plaintext highlighter-rouge">docker compose</code>.
Here’s the script:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Switch to root</span>
<span class="nb">sudo </span>su -
<span class="c"># Write script to file</span>
<span class="nb">cat</span> <span class="o">&lt;&lt;</span> <span class="no">EOF</span><span class="sh"> &gt; /usr/local/bin/docker-compose
#!/bin/bash
docker compose </span><span class="nv">$@</span><span class="sh">
</span><span class="no">EOF
</span><span class="c"># Make the script executable so that we can invoke it directly from the shell</span>
<span class="nb">chmod</span> +x /usr/local/bin/docker-compose
</code></pre></div></div>

<p>This avoids the error <code class="language-plaintext highlighter-rouge">docker-compose: command not found</code> which I faced, for example, trying to install https://github.com/amithkoujalgi/ollama-pdf-bot.</p>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="stream" /><category term="tooling" /><summary type="html"><![CDATA[The docker-compose command is missing from recent versions of Docker, replaced by a plugin built into Docker: docker compose. To restore compatibility with scripts which use docker-compose, we can create a wrapper script which forwards its arguments to docker compose. Here’s the script: # Switch to root sudo su - # Write script to file cat &lt;&lt; EOF &gt; /usr/local/bin/docker-compose #!/bin/bash docker compose $@ EOF # Make the script executable so that we can invoke it directly from the shell chmod +x /usr/local/bin/docker-compose]]></summary></entry><entry><title type="html">How to use task-spooler as a shared queueing system</title><link href="https://benjijang.com/posts/2023/12/shared-task-spooler/" rel="alternate" type="text/html" title="How to use task-spooler as a shared queueing system" /><published>2023-12-16T00:00:00-08:00</published><updated>2023-12-16T00:00:00-08:00</updated><id>https://benjijang.com/posts/2023/12/shared-task-spooler</id><content type="html" xml:base="https://benjijang.com/posts/2023/12/shared-task-spooler/"><![CDATA[<h1 id="tldr">TL;DR</h1>
<p>I set up a wrapper around task-spooler.
See the repository: <a href="https://github.com/bstee615/shared-task-spooler">https://github.com/bstee615/shared-task-spooler</a>.
To use it, create a file containing the below script and invoke it using the same arguments as task-spooler.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="c"># Dependency: sudo apt install -y task-spooler</span>
<span class="nv">TS_SOCKET</span><span class="o">=</span><span class="si">$(</span><span class="nb">dirname</span> <span class="nt">--</span> <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span><span class="si">)</span>/TS_SOCKET <span class="nb">exec </span>tsp <span class="nt">-L</span> <span class="s2">"</span><span class="nv">$USER</span><span class="s2">"</span> <span class="nv">$@</span>
</code></pre></div></div>

<p>Example usage:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@shared-box:~<span class="nv">$ </span>q <span class="nb">echo </span>hello
0
alice@shared-box:~<span class="nv">$ </span>q
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
0    finished   /tmp/ts-out.t7NbRs   0        0.00/0.00/0.00 <span class="o">[</span>alice]echo hello
alice@shared-box:~<span class="nv">$ </span><span class="nb">sudo </span>su - bill
bill@shared-box:~<span class="nv">$ </span>q
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
0    finished   /tmp/ts-out.t7NbRs   0        0.00/0.00/0.00 <span class="o">[</span>alice]echo hello
</code></pre></div></div>

<h1 id="why">Why?</h1>

<p>I share a machine with several labmates. This machine has a single high-powered GPU which we share for our experiments. However, only one of us (usually) can use it at a time. This means that if someone else is running an experiment, I have to write down my command, wait for their experiment to be over (which can run several hours), get pinged by them, then check back when they’re done and run by experiment. If I run my experiment without checking if the GPU is in-use, it can my program can experience an error, or worse, the other person’s in-progress program may experience an error and they’ll have to reset it. How can we efficiently run our experiments?</p>

<p>There are several shared queueing systems, such as <a href="https://slurm.schedmd.com/documentation.html">Slurm</a> or <a href="https://htcondor.org/">HTCondor</a> or <a href="https://sqs.sourceforge.net/">sqs</a>, but these are overkill - they tend to require a lot of work to set up and administrate. Additionally, these often require structuring your scripts around the format of the queueing tool, which slows down our work.
Instead, I set up a shared queue using <code class="language-plaintext highlighter-rouge">task-spooler</code> (<a href="https://vicerveza.homeunix.net/~viric/soft/ts/">link</a>)!
This solution generally works best when the users are somewhat technical and will not overrun the resources of the machine and interrupt other users’ projects. Also, it only works on one machine and can’t manage jobs distributed over a cluster. For more advanced systems which can limit resources or run on a cluster, see the alternatives listed above.</p>

<h1 id="how-i-developed-it">How I developed it</h1>

<p><code class="language-plaintext highlighter-rouge">task-spooler</code> tool is used to <em>spool</em> Bash scripts, or execute them in sequence.</p>

<p>However, while it’s intended to be used from multiple terminals, it isn’t intended for multiple users - each user has their own queue of tasks. This is because a different socket is created for each user.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@shared-box:~<span class="nv">$ </span>tsp
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
alice@shared-box:~<span class="nv">$ </span>tsp <span class="nb">echo </span>hello
0
alice@shared-box:~<span class="nv">$ </span>tsp
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
0    finished   /tmp/ts-out.YZiMxq   0        0.00/0.00/0.00 <span class="nb">echo </span>hello
alice@shared-box:~<span class="nv">$ </span><span class="nb">sudo </span>su - bill
bill@shared-box:~<span class="nv">$ </span>tsp
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
bill@shared-box:~<span class="nv">$ </span>tsp <span class="nb">echo </span>hello
0
bill@shared-box:~<span class="nv">$ </span>tsp
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
0    finished   /tmp/ts-out.LjNllC   0        0.00/0.00/0.00 <span class="nb">echo </span>hello
</code></pre></div></div>

<p>In order for two users to share a queue, they must share the same socket file, which is specified using the environment variable <code class="language-plaintext highlighter-rouge">TS_SOCKET</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@shared-box:~<span class="nv">$ TS_SOCKET</span><span class="o">=</span>/tmp/shared-socket tsp <span class="nb">echo </span>hello
0
alice@shared-box:~<span class="nv">$ TS_SOCKET</span><span class="o">=</span>/tmp/shared-socket tsp
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
0    finished   /tmp/ts-out.XItzuo   0        0.00/0.00/0.00 <span class="nb">echo </span>hello
alice@shared-box:~<span class="nv">$ </span><span class="nb">sudo </span>su - bill
alice@shared-box:~<span class="nv">$ TS_SOCKET</span><span class="o">=</span>/tmp/shared-socket tsp
c: cannot connect to the server
</code></pre></div></div>

<p>Uh oh, now <code class="language-plaintext highlighter-rouge">bill</code> ncannot access the same queue.
Looking into the source code, we see this is because when bill’s tsp tries to open the socket file, they receive the error <code class="language-plaintext highlighter-rouge">ENOACCESS</code>, which spouts the error message.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code></code></pre></div></div>

<p>Now we can make the socket file accessible by all users using <code class="language-plaintext highlighter-rouge">chmod 777</code>. If you want more restrictive permissions (for example, to restrict access to a specific group of users), you can use <a href="https://www.redhat.com/sysadmin/manage-permissions">Linux permission groups</a>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@shared-box:~<span class="nv">$ </span><span class="nb">chmod </span>777 /opt/shared-queue/TS_SOCKET
alice@shared-box:~<span class="nv">$ TS_SOCKET</span><span class="o">=</span>/tmp/shared-socket tsp <span class="nt">-L</span> <span class="s2">"</span><span class="nv">$USER</span><span class="s2">"</span> <span class="nb">echo </span>hello
0
alice@shared-box:~<span class="nv">$ TS_SOCKET</span><span class="o">=</span>/tmp/shared-socket tsp
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
0    finished   /tmp/ts-out.t7NbRs   0        0.00/0.00/0.00 <span class="nb">echo </span>hello
alice@shared-box:~<span class="nv">$ </span><span class="nb">sudo </span>su - bill
bill@shared-box:~<span class="nv">$ TS_SOCKET</span><span class="o">=</span>/tmp/shared-socket tsp
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
0    finished   /tmp/ts-out.t7NbRs   0        0.00/0.00/0.00 <span class="nb">echo </span>hello
</code></pre></div></div>

<p>Works great!</p>

<p>Finally, we want to distinguish jobs submitted by different users, so that each user can manage their own jobs in the shared queue.
We can do this by giving a label to the jobs using the <code class="language-plaintext highlighter-rouge">-L</code> option (see <a href="https://vicerveza.homeunix.net/~viric/soft/ts/#:~:text=-l%20%3Clab%3E%20name%20this%20task%20with%20a%20label%2C%20to%20be%20distinguished%20on%20listing">man pages</a>).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>alice@shared-box:~<span class="nv">$ TS_SOCKET</span><span class="o">=</span>/tmp/shared-socket tsp <span class="nt">-L</span> <span class="s2">"</span><span class="nv">$USER</span><span class="s2">"</span> <span class="nb">echo </span>hello
0
alice@shared-box:~<span class="nv">$ TS_SOCKET</span><span class="o">=</span>/tmp/shared-socket tsp
ID   State      Output               E-Level  Times<span class="o">(</span>r/u/s<span class="o">)</span>   Command <span class="o">[</span><span class="nv">run</span><span class="o">=</span>0/1]
0    finished   /tmp/ts-out.t7NbRs   0        0.00/0.00/0.00 <span class="o">[</span>alice]echo hello
</code></pre></div></div>

<p>The final version is wrapped into a convenient helper script <code class="language-plaintext highlighter-rouge">q</code> in this repository: <a href="https://github.com/bstee615/shared-task-spooler">https://github.com/bstee615/shared-task-spooler</a>.
To install it, you can clone it into a directory shared by all users (we have it in <code class="language-plaintext highlighter-rouge">/opt/shared-queue</code>), add the directory to the <code class="language-plaintext highlighter-rouge">PATH</code> variable, and start using <code class="language-plaintext highlighter-rouge">q</code>!</p>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="blog-post" /><category term="tooling" /><summary type="html"><![CDATA[TL;DR I set up a wrapper around task-spooler. See the repository: https://github.com/bstee615/shared-task-spooler. To use it, create a file containing the below script and invoke it using the same arguments as task-spooler. #!/bin/bash # Dependency: sudo apt install -y task-spooler TS_SOCKET=$(dirname -- "$0")/TS_SOCKET exec tsp -L "$USER" $@]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://benjijang.com/files/unsplash/super-snapper-sdTL4qTynfM-unsplash.jpg" /><media:content medium="image" url="https://benjijang.com/files/unsplash/super-snapper-sdTL4qTynfM-unsplash.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">friendship ended with `earlyoom`, now `nohang` is my best friend</title><link href="https://benjijang.com/friendship-ended-with-earlyoom/" rel="alternate" type="text/html" title="friendship ended with `earlyoom`, now `nohang` is my best friend" /><published>2023-02-13T00:00:00-08:00</published><updated>2023-02-13T00:00:00-08:00</updated><id>https://benjijang.com/friendship%20ended%20with%20earlyoom</id><content type="html" xml:base="https://benjijang.com/friendship-ended-with-earlyoom/"><![CDATA[<p><img src="/files/earlyoom_nohang.jpg" alt="friendship ended with earlyoom, now nohang is my best friend" /></p>

<p>I used to use <code class="language-plaintext highlighter-rouge">earlyoom</code> to ensure that my desktop PC will still keep running if a program hogs all the memory (e.g. loading a too-large dataset into memory). I recently couldn’t get <code class="language-plaintext highlighter-rouge">earlyoom</code> to work with Fedora 37, and while searching for a solution I found <code class="language-plaintext highlighter-rouge">nohang</code>. Here are some useful features of <code class="language-plaintext highlighter-rouge">nohang</code> which convinced me to switch:</p>

<ul>
  <li>Desktop notification when a program is killed.
<img src="/files/nohang_notification.jpg" alt="OOM notification" /></li>
  <li>Includes a demo command, <code class="language-plaintext highlighter-rouge">nohang --memload</code>, to safely test out a low-memory situation.</li>
  <li>Easy to build from source and sensible default config.</li>
</ul>

<p>Both are very useful and well-made programs, but <code class="language-plaintext highlighter-rouge">nohang</code> was better for my situation. Try them out!</p>
<ul>
  <li><a href="https://github.com/hakavlad/nohang">nohang@Github</a></li>
  <li><a href="https://github.com/rfjakob/earlyoom">earlyoom@Github</a></li>
</ul>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="stream" /><category term="tools" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Siamese network and triplet loss</title><link href="https://benjijang.com/Siamese-network-and-triplet-loss/" rel="alternate" type="text/html" title="Siamese network and triplet loss" /><published>2022-10-10T00:00:00-07:00</published><updated>2022-10-10T00:00:00-07:00</updated><id>https://benjijang.com/Siamese%20network%20and%20triplet%20loss</id><content type="html" xml:base="https://benjijang.com/Siamese-network-and-triplet-loss/"><![CDATA[<p>Siamese network is an architecture which runs two networks with shared weights (effectively runs the same network twice) on two different inputs simultaneously.
It is commonly trained with a contrastive loss such as triplet loss in order to draw together the representations of similar inputs and push apart the representations of contrasting inputs.</p>

<p>Define distance as the norm between the two encodings:
\(d(x_i, x_j) = ||f(x_i) - f(x_j)||^2\)</p>

<p>Goal: learn parameters so that</p>
<ul>
  <li>$x_i, x_j$ are the same person -&gt; $d(x_i, x_j)$ is small</li>
  <li>$x_i, x_j$ are the different people -&gt; $d(x_i, x_j)$ is large</li>
</ul>

<p>How to train? Triplet loss</p>
<ul>
  <li>Anchor $A$</li>
  <li>Positive $P$</li>
  <li>Negative $N$</li>
  <li>Want:
    <ul>
      <li>$d(A, P) \leq d(A, N)$</li>
      <li>$d(A, P) - d(A, N) \leq 0$</li>
      <li>This can be satisfied trivially with $d(*) = 0$.</li>
      <li>To prevent trivial solution, require the difference larger than a margin. $d(A, P) - d(A, N) + \alpha \leq 0$.</li>
    </ul>
  </li>
</ul>

<p>End up with Triplet loss $\mathcal L(A, P, N) = max(d(A, P) - d(A, N) + \alpha, 0)$.</p>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="stream" /><category term="nn" /><summary type="html"><![CDATA[Siamese network is an architecture which runs two networks with shared weights (effectively runs the same network twice) on two different inputs simultaneously. It is commonly trained with a contrastive loss such as triplet loss in order to draw together the representations of similar inputs and push apart the representations of contrasting inputs.]]></summary></entry><entry><title type="html">Get filepath of Bash activation script</title><link href="https://benjijang.com/Get-filepath-of-Bash-activation-script/" rel="alternate" type="text/html" title="Get filepath of Bash activation script" /><published>2022-07-25T00:00:00-07:00</published><updated>2022-07-25T00:00:00-07:00</updated><id>https://benjijang.com/Get%20filepath%20of%20Bash%20activation%20script</id><content type="html" xml:base="https://benjijang.com/Get-filepath-of-Bash-activation-script/"><![CDATA[<p>Use <code class="language-plaintext highlighter-rouge">${BASH_SOURCE[0]}</code> to reference the filepath of a Bash script.
Unlike <code class="language-plaintext highlighter-rouge">$0</code>, this works if the script is called via <code class="language-plaintext highlighter-rouge">bash script.sh</code> or <code class="language-plaintext highlighter-rouge">source script.sh</code>.</p>

<p>Source: https://stackoverflow.com/a/8912075</p>

<p>Example:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># activate.sh</span>

<span class="nv">root</span><span class="o">=</span><span class="si">$(</span><span class="nb">realpath</span> <span class="si">$(</span><span class="nb">dirname</span> <span class="k">${</span><span class="nv">BASH_SOURCE</span><span class="p">[0]</span><span class="k">}</span><span class="si">))</span>
<span class="nb">source</span> <span class="nv">$root</span>/venv/bin/activate
<span class="nb">export </span><span class="nv">PYTHONPATH</span><span class="o">=</span><span class="s2">"</span><span class="nv">$root</span><span class="s2">:</span><span class="nv">$PYTHONPATH</span><span class="s2">"</span>
</code></pre></div></div>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="stream" /><category term="tips" /><summary type="html"><![CDATA[Use ${BASH_SOURCE[0]} to reference the filepath of a Bash script. Unlike $0, this works if the script is called via bash script.sh or source script.sh.]]></summary></entry><entry><title type="html">webcam-mods for Linux background blur &amp;amp; swap</title><link href="https://benjijang.com/webcam-mods-for-Linux-background-blur-&-swap/" rel="alternate" type="text/html" title="webcam-mods for Linux background blur &amp;amp; swap" /><published>2022-07-07T00:00:00-07:00</published><updated>2022-07-07T00:00:00-07:00</updated><id>https://benjijang.com/webcam-mods%20for%20Linux%20background%20blur%20&amp;%20swap</id><content type="html" xml:base="https://benjijang.com/webcam-mods-for-Linux-background-blur-&amp;-swap/"><![CDATA[<p><a href="https://github.com/hamidzr/webcam-mods">webcam-mods</a> is the best method I have found for webcam background blur/swap on Linux. I use this for my meetings on Google Meet and Webex.</p>

<p>Repo: https://github.com/hamidzr/webcam-mods
Install globally:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>git+https://github.com/hamidzr/webcam-mods@master
</code></pre></div></div>

<p>Original</p>

<p><img src="/files/2022-07-07-webcam-mods for Linux background blur &amp; swap.md-Pasted image 20220707144227.png" alt="Pasted image 20220707144227.png" /></p>

<p>Cropped: <code class="language-plaintext highlighter-rouge">webcam_mods crop-cam</code>. I was impressed with the interactive cropping mode which allowed me to crop to my profile pretty easily. The crop settings are saved to disk for future runs.</p>

<p><img src="/files/2022-07-07-webcam-mods for Linux background blur &amp; swap.md-Pasted image 20220707144141.png" alt="Pasted image 20220707144141.png" /></p>

<p>Cropped and blurred: <code class="language-plaintext highlighter-rouge">webcam_mods bg-blur</code></p>

<p><img src="/files/2022-07-07-webcam-mods for Linux background blur &amp; swap.md-Pasted image 20220707144301.png" alt="Pasted image 20220707144301.png" /></p>

<p>Cropped with bg: <code class="language-plaintext highlighter-rouge">webcam_mods bg-swap</code></p>

<p><img src="/files/2022-07-07-webcam-mods for Linux background blur &amp; swap.md-Pasted image 20220707144332.png" alt="Pasted image 20220707144332.png" /></p>

<p>Video feed was displayed with <a href="https://ffmpeg.org/ffplay.html">ffplay</a>.
Runner-ups that I tried:</p>
<ul>
  <li><a href="https://github.com/fangfufu/Linux-Fake-Background-Webcam">Linux-Fake-Background-Webcam</a>. I found its blur didn’t work quite as well (background and limbs pop in/out), so it was distracting in meetings.</li>
  <li><a href="https://github.com/RazZziel/fakecam">fakecam</a>. It was a bit difficult to install (see issue <a href="https://github.com/RazZziel/fakecam/issues/2">here</a>).</li>
</ul>]]></content><author><name>Benjamin Steenhoek</name><email>bensteenhoek@microsoft.com</email><uri>https://www.microsoft.com/en-us/research/people/bensteenhoek/</uri></author><category term="stream" /><category term="tools" /><summary type="html"><![CDATA[webcam-mods is the best method I have found for webcam background blur/swap on Linux. I use this for my meetings on Google Meet and Webex.]]></summary></entry></feed>