<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Kevin Tang</title>
    <description>The latest articles on DEV Community by Kevin Tang (@kevintang).</description>
    <link>https://dev.to/kevintang</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3792406%2F3c1ad0a6-3f86-4168-90a3-9277d3f57802.png</url>
      <title>DEV Community: Kevin Tang</title>
      <link>https://dev.to/kevintang</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kevintang"/>
    <language>en</language>
    <item>
      <title>I Ran DeepSeek V4 Flash Across Two DGX Sparks Over Ethernet</title>
      <dc:creator>Kevin Tang</dc:creator>
      <pubDate>Sun, 06 Sep 2026 18:11:55 +0000</pubDate>
      <link>https://dev.to/kevintang/i-ran-deepseek-v4-flash-across-two-dgx-sparks-over-ethernet-158b</link>
      <guid>https://dev.to/kevintang/i-ran-deepseek-v4-flash-across-two-dgx-sparks-over-ethernet-158b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR: Two 128GB GB10 computers can run the official FP8 DeepSeek V4 Flash 0731 checkpoint as one tensor-parallel server over their built-in 10Gb Ethernet ports. My improvised link was roughly half as fast as the recipe's published 200G results, but it worked reliably: about 30 tokens per second for one stream, 85 tokens per second aggregate across six short requests, and a configured 1M-token context ceiling. The 200G cable is a performance requirement, not a functional requirement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp26uxrkreg67ntjsbm6o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp26uxrkreg67ntjsbm6o.webp" alt="Sparkfetch terminal card showing the NVIDIA DGX Spark and ASUS Ascent GX10 linked over direct 10GbE as one tensor-parallel DeepSeek server." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The 37.4 tok/s figure above is the median from my &lt;code&gt;llm-gym&lt;/code&gt; comparison; the serving sweep later in this post measured 29–31 tok/s.&lt;/p&gt;

&lt;p&gt;I had two 128GB GB10 computers, a 167GB model, and the wrong cable.&lt;/p&gt;

&lt;p&gt;The computers were an NVIDIA DGX Spark and an ASUS Ascent GX10. They use the same Grace Blackwell GB10 platform, so together they give me 256GB of aggregate unified memory. The model was &lt;code&gt;deepseek-ai/DeepSeek-V4-Flash-0731&lt;/code&gt;: 284 billion target-model parameters, 13 billion active at a time, plus an attached draft model for speculative decoding. The Hugging Face repository reports 304 billion parameters when that draft module is included.&lt;sup&gt;[1]&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9e0wlhe8nh296o5h73dj.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F9e0wlhe8nh296o5h73dj.webp" alt="The physical two-node setup: an NVIDIA DGX Spark stacked above an ASUS Ascent GX10." width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On one Spark, I had already run a heavily compressed community build: an approximately 81GiB IQ2XXS/Q2 target model plus a 6.5GiB Q2K/Q8 draft model. It was surprisingly good. It scored 197 out of 199 on my &lt;code&gt;llm-gym&lt;/code&gt; core benchmark and generated about 29.7 tokens per second.&lt;/p&gt;

&lt;p&gt;But I wanted to run the official source checkpoint. It is commonly described as the FP8 model, which is fair shorthand, although “unquantized” is not. Its 48 weight shards occupy exactly 166,886,535,336 bytes, and its tensors use a mixture of FP8 E4M3 block-scaled storage, BF16, FP32, and packed integers. That does not fit comfortably on one 128GB system once the serving engine and KV cache need memory too.&lt;/p&gt;

&lt;p&gt;Two boxes solve the capacity problem. NVIDIA even documents a two-Spark setup. The expected connection is the dedicated ConnectX-7 fabric: a QSFP cable carrying 200Gb/s Ethernet, separate from the ordinary RJ45 10GbE port.&lt;sup&gt;[2]&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Unfortunately, I had bought a QSFPTEK 400G QSFP-DD cable that these machines did not like. Both ends reported the same brutally clear status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cable unplugged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ConnectX-7 device would briefly enumerate during boot and then power down. A community-reported compatible 200G QSFP56 DAC was on the way, but I did not feel like waiting.&lt;/p&gt;

&lt;p&gt;Then I looked at the two unused RJ45 ports.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ethernet is still a network
&lt;/h2&gt;

&lt;p&gt;Each Spark has a built-in RJ45 10GBASE-T interface.&lt;sup&gt;[3]&lt;/sup&gt; I connected the machines directly with an ordinary Cat6/Cat6a Ethernet cable—no switch—and assigned a tiny point-to-point subnet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nvidia-spark  enP7s7  10.10.10.1/30  MTU 9000
asus-spark    enP7s7  10.10.10.2/30  MTU 9000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I made the configuration persistent with NetworkManager. The commands looked like this, with the address changed for the second machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nmcli connection add &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;type &lt;/span&gt;ethernet &lt;span class="se"&gt;\&lt;/span&gt;
  ifname enP7s7 &lt;span class="se"&gt;\&lt;/span&gt;
  con-name spark-direct-10g &lt;span class="se"&gt;\&lt;/span&gt;
  ipv4.method manual &lt;span class="se"&gt;\&lt;/span&gt;
  ipv4.addresses 10.10.10.1/30 &lt;span class="se"&gt;\&lt;/span&gt;
  ipv4.never-default &lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ipv6.method disabled &lt;span class="se"&gt;\&lt;/span&gt;
  802-3-ethernet.mtu 9000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before involving CUDA, Docker, or DeepSeek, I tested the boring part. A 1GiB TCP transfer sustained about 9.9Gb/s from the NVIDIA box to the ASUS and 9.3Gb/s in reverse. Two simultaneous 84GB model transfers ran at roughly 1.0–1.1GB/s without link errors.&lt;/p&gt;

&lt;p&gt;That established something useful: the cable and NICs were doing essentially everything 10GbE could do. If inference was slow later, I would not have to wonder whether I had accidentally negotiated at 1Gb/s.&lt;/p&gt;

&lt;p&gt;I considered Wi-Fi too. It technically connected, but the machines were getting 702–867Mb/s PHY rates and wildly variable 68–79ms average ping times. That might be fine for sending prompts to a server. It is terrible for making two GPUs stop and synchronize repeatedly inside every generated token. I abandoned that idea quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convincing the recipe to use TCP
&lt;/h2&gt;

&lt;p&gt;I used MiaAI Lab's two-DGX-Spark recipe, which packages vLLM tensor parallelism, DeepSeek's attached speculative draft model, and a 1M-token context configuration.&lt;sup&gt;[4]&lt;/sup&gt; The container was &lt;code&gt;ghcr.io/anemll/dspark-vllm-gx10:0.1.1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The recipe expected the ConnectX/RoCE path. Its startup logic tried to discover an InfiniBand or RoCE HCA and a GID before NCCL had a chance to fall back to an IP socket. With no working QSFP link, startup failed before the interesting work began.&lt;/p&gt;

&lt;p&gt;I initially expected to patch the launcher. It turned out I did not need to. These were the important environment settings on the head node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;WORKER_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.10.10.2
&lt;span class="nv"&gt;MASTER_ADDR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.10.10.1
&lt;span class="nv"&gt;MASTER_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;25000

&lt;span class="nv"&gt;NCCL_NET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Socket
&lt;span class="nv"&gt;NCCL_IB_DISABLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;NCCL_IB_HCA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;unused
&lt;span class="nv"&gt;NCCL_IB_GID_AUTO&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="nv"&gt;NCCL_SOCKET_IFNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;enP7s7
&lt;span class="nv"&gt;TP_SOCKET_IFNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;enP7s7
&lt;span class="nv"&gt;GLOO_SOCKET_IFNAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;enP7s7

&lt;span class="nv"&gt;VLLM_HOST_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.10.10.1
&lt;span class="nv"&gt;WORKER_VLLM_HOST_IP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10.10.10.2
&lt;span class="nv"&gt;HF_HUB_OFFLINE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1

&lt;span class="nv"&gt;MAX_MODEL_LEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1048576
&lt;span class="nv"&gt;MAX_NUM_SEQS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;6
&lt;span class="nv"&gt;MTP_NUM_TOKENS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NCCL_NET=Socket&lt;/code&gt; selects NCCL's TCP/IP transport, while &lt;code&gt;NCCL_SOCKET_IFNAME&lt;/code&gt; pins it to the direct 10GbE interface.&lt;sup&gt;[5]&lt;/sup&gt; Disabling IB and automatic GID discovery kept the launcher away from the disconnected ConnectX device. &lt;code&gt;GLOO_SOCKET_IFNAME&lt;/code&gt; and the two vLLM host addresses made the rest of the distributed stack use the same route.&lt;/p&gt;

&lt;p&gt;Both nodes also needed a complete local copy of the model. The 167GB checkpoint does not cross the cable at startup on every run, and it definitely does not cross it for every prompt. I staged the files on both machines and enabled Hugging Face offline mode so neither worker could quietly download a missing shard during launch.&lt;/p&gt;

&lt;p&gt;Then the logs finally said what I wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Bootstrap: Using enP7s7:10.10.10.1
NET/Socket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full server took about 8 minutes 25 seconds to start. Model loading took roughly 244 seconds on the head and 153 seconds on the worker. Each tensor-parallel rank held about 79.2GiB of weights and allocated about 101GB of GPU/unified memory. The remaining 13.78GiB KV cache held approximately 1.96 million tokens in total—about 1.87 simultaneous full-context requests in the mathematical sense, though I would not actually serve it that way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What crosses the cable
&lt;/h2&gt;

&lt;p&gt;This is the part I had initially pictured incorrectly.&lt;/p&gt;

&lt;p&gt;The model is not copied from one Spark to the other for every prompt. Tensor parallelism splits each large matrix operation across both ranks. Each machine keeps its own weight shard resident in memory, computes a partial result, and then NCCL exchanges or reduces intermediate tensors so the next operation can continue.&lt;/p&gt;

&lt;p&gt;That communication happens over and over through the network. A 200Gb/s link has a theoretical line rate of 25GB/s. A 10Gb/s link has 1.25GB/s. The 200G fabric therefore has twenty times the raw bandwidth, along with lower latency and less TCP/IP overhead.&lt;/p&gt;

&lt;p&gt;But twenty times the network bandwidth does not imply twenty times the token rate. Each token also spends time doing local memory reads, GPU math, routing through the mixture-of-experts layers, and speculative verification. Some communication can overlap with computation. The network only determines the portion of time spent waiting at distributed synchronization points.&lt;/p&gt;

&lt;p&gt;There is another wrinkle specific to DGX Spark: NVIDIA says GPUDirect RDMA is not supported on its unified-memory architecture. Even the ConnectX path uses host buffers rather than letting the NIC directly DMA into GPU memory.&lt;sup&gt;[6]&lt;/sup&gt; The 200G connection still wins decisively on bandwidth, latency, RoCE behavior, and CPU overhead. It just is not magic direct NIC-to-GPU transfer on this machine.&lt;/p&gt;

&lt;p&gt;Once I understood that, the 10GbE result stopped looking mysterious. NCCL supports a socket transport. I gave it a valid, fast-enough IP interface. Tensor parallelism cares that its collectives complete correctly; it does not care whether the connector looks impressive.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;p&gt;I ran a bounded 256-output-token sweep across prompt lengths from 256 to 131,072 tokens and concurrency levels from one to six. All 20 test cases finished without NCCL errors, CUDA errors, out-of-memory failures, or worker restarts.&lt;/p&gt;

&lt;p&gt;Here are the most useful points from that sweep:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;TTFT&lt;/th&gt;
&lt;th&gt;Per-stream decode&lt;/th&gt;
&lt;th&gt;Aggregate throughput&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;256 tokens&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.42s&lt;/td&gt;
&lt;td&gt;30.47 tok/s&lt;/td&gt;
&lt;td&gt;29.02 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256 tokens&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1.97s&lt;/td&gt;
&lt;td&gt;16.84 tok/s&lt;/td&gt;
&lt;td&gt;85.48 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,048 tokens&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0.51s&lt;/td&gt;
&lt;td&gt;31.05 tok/s&lt;/td&gt;
&lt;td&gt;29.24 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8,192 tokens&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;8.63s&lt;/td&gt;
&lt;td&gt;28.65 tok/s&lt;/td&gt;
&lt;td&gt;14.57 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;131,072 tokens&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;145.64s&lt;/td&gt;
&lt;td&gt;30.17 tok/s&lt;/td&gt;
&lt;td&gt;1.66 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;131,072 tokens&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;496.73s&lt;/td&gt;
&lt;td&gt;0.75 tok/s&lt;/td&gt;
&lt;td&gt;1.79 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For ordinary chat-sized prompts, the experience was completely usable. Around 30 tokens per second is faster than I read. Six simultaneous short requests increased total throughput to 85.5 tokens per second, although each individual stream slowed and TTFT rose.&lt;/p&gt;

&lt;p&gt;Long context exposed the real cost. Once generation began, even the 131K single request decoded at 30.2 tokens per second. Waiting 145.6 seconds for the first token was the problem. Six 131K requests were technically possible, but nearly eight and a half minutes to first token is not a serious interactive configuration.&lt;/p&gt;

&lt;p&gt;That may still be good enough for overnight runs. If six long research or coding jobs are going to run for hours, I care more that they finish by morning than whether the first token appears in eight minutes. The same setup can be a bad interactive chat server and a useful batch machine.&lt;/p&gt;

&lt;p&gt;I also ran vLLM's standardized benchmark with 2,048 input tokens, 128 output tokens, and 12 requests. At concurrency one it produced 31.49 output tokens per second with median TTFT of 583ms. At concurrency six it reached 72.03 aggregate output tokens per second with median TTFT of 1.79 seconds. The five-token speculative draft acceptance rate stayed around 65–67 percent.&lt;/p&gt;

&lt;p&gt;How much did the cable cost? The recipe's matched published 200G sweep reported roughly 64–75 single-stream decode tokens per second depending on prompt length.&lt;sup&gt;[4]&lt;/sup&gt; My 10GbE setup delivered 29–31. In other words, ordinary Ethernet preserved about 40–46 percent of the intended single-stream speed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prompt&lt;/th&gt;
&lt;th&gt;Published 200G&lt;/th&gt;
&lt;th&gt;My 10GbE&lt;/th&gt;
&lt;th&gt;10GbE share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;256 tokens&lt;/td&gt;
&lt;td&gt;75.4 tok/s&lt;/td&gt;
&lt;td&gt;30.5 tok/s&lt;/td&gt;
&lt;td&gt;40%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2,048 tokens&lt;/td&gt;
&lt;td&gt;68.8 tok/s&lt;/td&gt;
&lt;td&gt;31.1 tok/s&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8,192 tokens&lt;/td&gt;
&lt;td&gt;73.9 tok/s&lt;/td&gt;
&lt;td&gt;28.7 tok/s&lt;/td&gt;
&lt;td&gt;39%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;131,072 tokens&lt;/td&gt;
&lt;td&gt;65.2 tok/s&lt;/td&gt;
&lt;td&gt;30.2 tok/s&lt;/td&gt;
&lt;td&gt;46%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That comparison is the closest thing here to a cable benchmark, but it is still not laboratory-perfect. It compares my machines and software state against the recipe authors' published run, not the same two machines with one variable changed. I will run the clean 10G-versus-200G A/B test when the correct DAC arrives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The higher-precision model versus one box
&lt;/h2&gt;

&lt;p&gt;I reran the same 199-question &lt;code&gt;llm-gym&lt;/code&gt; core benchmark so I could compare this system with the compressed model I had been using on one Spark:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Deployment&lt;/th&gt;
&lt;th&gt;Model format&lt;/th&gt;
&lt;th&gt;Raw score&lt;/th&gt;
&lt;th&gt;Weighted score&lt;/th&gt;
&lt;th&gt;Median generation speed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One Spark&lt;/td&gt;
&lt;td&gt;Community IQ2XXS/Q2&lt;/td&gt;
&lt;td&gt;197/199&lt;/td&gt;
&lt;td&gt;98.1&lt;/td&gt;
&lt;td&gt;29.7 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Two Sparks over 10GbE&lt;/td&gt;
&lt;td&gt;Official FP8 source checkpoint&lt;/td&gt;
&lt;td&gt;196/199&lt;/td&gt;
&lt;td&gt;96.0&lt;/td&gt;
&lt;td&gt;37.4 tok/s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two-box system was 7.7 tokens per second faster on this workload, or about 26 percent. The higher-fidelity weights did not produce a visible accuracy win on this small benchmark. Both models missed slightly different “flippy” questions, and a one-question raw difference is not enough evidence to claim either version is smarter.&lt;/p&gt;

&lt;p&gt;That result was useful anyway. The compressed single-box model is not merely a consolation prize—it is an excellent practical configuration. The official FP8 model gives me higher-fidelity weights, the full intended serving path, more memory headroom, and better generation speed, but it spends a lot of its two-node advantage waiting on 10GbE.&lt;/p&gt;

&lt;p&gt;This is also why “two boxes are faster” is too simple. The comparison changes the model format, serving engine, speculative draft, tensor parallelism, and network at the same time. The 26 percent is the result I observed, not an isolated claim about any one of those variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody recommends this
&lt;/h2&gt;

&lt;p&gt;NVIDIA is not hiding the Ethernet port. It is documented as a separate 10GbE interface. The problem is that the validated cluster path is the 200G ConnectX fabric, so all the setup tooling, diagrams, and troubleshooting instructions naturally assume that topology. NVIDIA's cluster assistant even uses roughly 184Gb/s as the lower bound for a healthy expected link.&lt;sup&gt;[2]&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;That is reasonable. If a vendor published “you can use the RJ45 port,” many people would interpret that as “the RJ45 port performs the same.” It does not. My short-prompt single-stream result lost more than half the published token rate, and long-context TTFT became painful.&lt;/p&gt;

&lt;p&gt;The software defaults reinforce the assumption. The recipe went looking for an HCA and a GID because every normal deployment of this configuration has them. The socket path was already inside NCCL, but I had to explicitly route the rest of the launcher around its RoCE expectations.&lt;/p&gt;

&lt;p&gt;So this is not the setup I would recommend buying on purpose. It is the setup I would recommend trying if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The correct 200G cable is delayed or unavailable.&lt;/li&gt;
&lt;li&gt;You want to prove that both nodes, model shards, Docker image, SSH setup, and vLLM configuration work before debugging the high-speed fabric.&lt;/li&gt;
&lt;li&gt;You care more about fitting the model than maximizing its speed.&lt;/li&gt;
&lt;li&gt;You already own the two machines and an Ethernet cable, which makes the experiment effectively free.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For latency-sensitive serving, heavy concurrency, or long-context prefill, buy the compatible 200G DAC. For Wi-Fi, I would not tensor-parallelize at all—I would run independent models and route whole requests between them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;The interesting distinction was not between “supported” and “unsupported.” It was between capacity, correctness, and performance.&lt;/p&gt;

&lt;p&gt;Two machines gave me the capacity to hold the official checkpoint. NCCL's TCP socket transport gave me correctness over an ordinary network. The expensive fabric gives performance by reducing the time both ranks spend waiting for each other.&lt;/p&gt;

&lt;p&gt;I am still going to install the 200G cable when it arrives. I now know exactly what it buys me: not the ability to run the model, but time back at every synchronization point.&lt;/p&gt;

&lt;p&gt;Until then, an ordinary Ethernet cable is carrying DeepSeek between two little boxes on my desk, which is much funnier than it has any right to be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Citations
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a id="cite-1"&gt;&lt;/a&gt;&lt;a href="https://huggingface.co/deepseek-ai/DeepSeek-V4-Flash-0731" rel="noopener noreferrer"&gt;DeepSeek V4 Flash 0731 model repository&lt;/a&gt;, DeepSeek AI. ↩
&lt;/li&gt;
&lt;li&gt;
&lt;a id="cite-2"&gt;&lt;/a&gt;&lt;a href="https://build.nvidia.com/spark/connect-two-sparks/stacked-sparks" rel="noopener noreferrer"&gt;Connecting two DGX Sparks&lt;/a&gt; and &lt;a href="https://docs.nvidia.com/sync/latest/cluster-assistant.html" rel="noopener noreferrer"&gt;DGX Spark Cluster Assistant&lt;/a&gt;, NVIDIA. ↩
&lt;/li&gt;
&lt;li&gt;
&lt;a id="cite-3"&gt;&lt;/a&gt;&lt;a href="https://docs.nvidia.com/dgx/dgx-spark/hardware.html" rel="noopener noreferrer"&gt;DGX Spark hardware overview&lt;/a&gt;, NVIDIA. ↩
&lt;/li&gt;
&lt;li&gt;
&lt;a id="cite-4"&gt;&lt;/a&gt;&lt;a href="https://github.com/MiaAI-Lab/DeepSeek-v4-Flash-DSpark-2x-DGX-Spark" rel="noopener noreferrer"&gt;DeepSeek V4 Flash DSpark on two DGX Sparks&lt;/a&gt;, MiaAI Lab. ↩
&lt;/li&gt;
&lt;li&gt;
&lt;a id="cite-5"&gt;&lt;/a&gt;&lt;a href="https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/env.html" rel="noopener noreferrer"&gt;NCCL environment variables&lt;/a&gt;, NVIDIA. ↩
&lt;/li&gt;
&lt;li&gt;
&lt;a id="cite-6"&gt;&lt;/a&gt;&lt;a href="https://docs.nvidia.com/dgx/dgx-spark-porting-guide/porting/cuda.html" rel="noopener noreferrer"&gt;DGX Spark CUDA porting guide: GPUDirect RDMA limitations&lt;/a&gt;, NVIDIA. ↩
&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>ai</category>
      <category>hardware</category>
      <category>localllm</category>
      <category>networking</category>
    </item>
    <item>
      <title>Your iPhone Is an OpenAI-Compatible LLM Server Now</title>
      <dc:creator>Kevin Tang</dc:creator>
      <pubDate>Sun, 06 Sep 2026 18:11:15 +0000</pubDate>
      <link>https://dev.to/kevintang/your-iphone-is-an-openai-compatible-llm-server-now-20d9</link>
      <guid>https://dev.to/kevintang/your-iphone-is-an-openai-compatible-llm-server-now-20d9</guid>
      <description>&lt;p&gt;&lt;em&gt;TL;DR: &lt;a href="https://apps.apple.com/us/app/local-llm-server/id6757007308?ct=kevinmd-202607" rel="noopener noreferrer"&gt;Local LLM Server&lt;/a&gt; turns an iPhone, iPad, or Mac into an LLM server: Apple's on-device Foundation Models exposed over an OpenAI- and Ollama-compatible HTTP API. Any existing client library points at your phone's LAN address and just works. Free with ads; &lt;a href="https://apps.apple.com/us/app/local-llm-server-pro/id6758336829?ct=kevinmd-202607" rel="noopener noreferrer"&gt;Pro is $1.99&lt;/a&gt; without them.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Apple Intelligence ships a ~3B-parameter language model on every recent iPhone, running locally on the neural engine. It's genuinely capable for structured tasks—classification, extraction, summaries—and it costs nothing per token. The catch: Apple exposes it as a Swift framework. If your tooling speaks HTTP—Python scripts, LangChain, DSPy, a curl one-liner, literally the entire LLM ecosystem—there's no door in.&lt;/p&gt;

&lt;p&gt;Local LLM Server is that door. Open the app, pick a port, hit Start Server, and the device serves at &lt;code&gt;http://&amp;lt;your-lan-ip&amp;gt;:8080&lt;/code&gt; with the two API shapes everything already understands: OpenAI's (&lt;code&gt;/v1/chat/completions&lt;/code&gt; and friends) and Ollama's. Your OpenAI client library doesn't know it's talking to a phone.&lt;/p&gt;

&lt;p&gt;&lt;a href="/images/local-llm-server-running-apple-intelligence.png" class="article-body-image-wrapper"&gt;&lt;img src="/images/local-llm-server-running-apple-intelligence.png" alt="Local LLM Server running on an iPhone with a Powered by Apple Intelligence label and a LAN address" width="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The phone is the server.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "compatible" actually covers
&lt;/h2&gt;

&lt;p&gt;The parts I'm most pleased with, because they're the parts most wrappers skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured outputs&lt;/strong&gt; — &lt;code&gt;response_format&lt;/code&gt; with a JSON schema drives Apple's guided generation, so you get valid JSON back, not JSON-shaped prose.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool / function calling&lt;/strong&gt; — the standard OpenAI client-executed loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real token counts&lt;/strong&gt; — &lt;code&gt;usage&lt;/code&gt; comes from the actual tokenizer, with a &lt;code&gt;context_length_exceeded&lt;/code&gt; preflight instead of a mystery truncation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embeddings&lt;/strong&gt; — &lt;code&gt;/v1/embeddings&lt;/code&gt; served from Apple's contextual embedding model, also on-device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A browser playground&lt;/strong&gt; at &lt;code&gt;GET /&lt;/code&gt;, optional Bearer auth, LAN or localhost-only binding, and App Intents so Shortcuts can start and stop the server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="/images/local-llm-server-chat-clients.webp" class="article-body-image-wrapper"&gt;&lt;img src="/images/local-llm-server-chat-clients.webp" alt="OpenAI and Ollama-compatible chat clients prompting the local Apple Intelligence model through Local LLM Server" width="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The compatibility test I actually care about: existing chat clients talking to the phone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There's a 90-test suite behind the API surface. One user wired the server into DSPy for prompt optimization against the on-device model, which is exactly the kind of thing I hoped people would do and did not want to explain was impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother, when you could call a cloud API?
&lt;/h2&gt;

&lt;p&gt;Three answers, in my order of caring: it's &lt;em&gt;private&lt;/em&gt; (prompts never leave the device—for some data that's not a preference, it's the requirement); it's &lt;em&gt;free at the margin&lt;/em&gt; (bulk classification jobs cost zero tokens); and it's &lt;em&gt;always with you&lt;/em&gt; (the server is in your pocket, on a plane, in a dead zone). The honest counterweight: it's a 3B model. It will not write your novel. For extraction, tagging, structured summaries, and glue-code intelligence, it's shockingly serviceable—and those are most of what scripts actually need.&lt;/p&gt;

&lt;p&gt;&lt;a href="/images/local-llm-server-on-device-proof.png" class="article-body-image-wrapper"&gt;&lt;img src="/images/local-llm-server-on-device-proof.png" alt="Server Information screen showing on-device processing and no cloud data transfer" width="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The important line: on-device processing, no cloud round-trip.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Free vs Pro
&lt;/h2&gt;

&lt;p&gt;The free app is ad-supported; &lt;a href="https://apps.apple.com/us/app/local-llm-server-pro/id6758336829?ct=kevinmd-202607" rel="noopener noreferrer"&gt;Local LLM Server Pro&lt;/a&gt; is $1.99, one-time, no ads. Same server, same API. Pro briefly hit #5 on the US paid Developer Tools chart—a thin, volatile chart, so I won't pretend that's fame, but it did tell me this audience would rather pay two dollars than look at a banner, which matches how I feel about banners.&lt;/p&gt;

&lt;p&gt;&lt;a href="/images/local-llm-server-pro-app-store-rank.webp" class="article-body-image-wrapper"&gt;&lt;img src="/images/local-llm-server-pro-app-store-rank.webp" alt="App Store page showing Local LLM Server Pro at #5 in Developer Tools" width="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tiny chart, still a real signal: Local LLM Server Pro at #5 in Developer Tools.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Requires iOS/iPadOS/macOS 26+ on Apple Intelligence-capable hardware (that's where the on-device model lives). If you've got a spare device on your desk, it's a free local inference endpoint you didn't know you owned.&lt;/p&gt;

</description>
      <category>localllmserver</category>
      <category>appleintelligence</category>
      <category>localai</category>
      <category>developertools</category>
    </item>
    <item>
      <title>The Business Card That Lights Up When You Tap It</title>
      <dc:creator>Kevin Tang</dc:creator>
      <pubDate>Sun, 06 Sep 2026 18:09:51 +0000</pubDate>
      <link>https://dev.to/kevintang/the-business-card-that-lights-up-when-you-tap-it-4660</link>
      <guid>https://dev.to/kevintang/the-business-card-that-lights-up-when-you-tap-it-4660</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR: I made a circuit-board business card with an NFC chip and a batteryless LED powered by the phone tapping it. The hardware worked, but ordinary iPhone NFC apps couldn't format the blank NTAG I²C Plus chip. I wrote a tiny Core NFC app that sends the chip's native commands directly, and now each tap opens my site and lights Charizard's tail.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I wanted a business card that wasn't a piece of paper. Not out of some deep philosophy—I just thought it would be fun to hand someone a circuit board. So I designed one: credit-card sized, glossy black, with gold artwork and my name on it. Standard so far. The two things that make it not-just-a-card are both invisible until you bring a phone near it.&lt;/p&gt;

&lt;p&gt;The first is an NFC chip. Tap the card to a phone and it opens &lt;a href="https://kevintang.xyz" rel="noopener noreferrer"&gt;kevintang.xyz&lt;/a&gt;—the same technology as a tap-to-pay card or a transit pass, running at 13.56 MHz&lt;sup&gt;[1]&lt;/sup&gt;. There's a spiral of copper wound seven times around the edge of the board; that's the antenna, and getting it to resonate at the right frequency is most of the work. That part I expected.&lt;/p&gt;

&lt;p&gt;The second thing is the part I can't stop thinking about. There's a little Charizard printed on the card, and a red LED sits right at the tip of its tail—so tap the card to a phone and its tail catches fire. There's no battery on the card. There's no coin cell, no wire, nothing to charge. The LED is running entirely on energy the phone is throwing off.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78yh6so69c14x5u3xyw1.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F78yh6so69c14x5u3xyw1.webp" alt="A black circuit-board business card styled like a component datasheet, a small red LED glowing near the top; below it a phone runs a custom " width="800" height="1422"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The card mid-write—my own little app pushing the URL onto the chip—with the harvest LED lit from nothing but the phone's field. Several more waiting their turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  An object that borrows power
&lt;/h2&gt;

&lt;p&gt;Here's what's actually happening, and it took me a while to stop finding it magical (I haven't, really). An NFC phone doesn't just listen for a tag—it broadcasts a strong radio field to power the tag, because the tag has no battery either. The chip in a normal NFC sticker wakes up on that borrowed energy just long enough to answer, then goes dark the moment the phone leaves. What I didn't appreciate until I built one is that some chips will hand you the leftover power on a pin, to do whatever you want with. NXP's datasheet lists a typical harvested output of about 5 milliamps at 2 volts under one test setup&lt;sup&gt;[2]&lt;/sup&gt;—not much, but plenty to light an LED, or blink a low-power sensor, or wake a tiny microcontroller for a fraction of a second.&lt;/p&gt;

&lt;p&gt;And the way it answers is stranger than it sounds: the tag never transmits a signal of its own. It replies by changing how much of the phone's field it absorbs, instant to instant, and the phone feels those little tugs on its own signal and reads them as ones and zeros. The analogy I keep coming back to is a mirror. A mirror makes no light—it only reflects a source. A passive NFC tag makes no radio of its own—it reflects the phone's, flickering it into a message. The phone brings all the energy; the card just decides what to bounce back.&lt;/p&gt;

&lt;p&gt;(This, incidentally, is the NFC-versus-RFID distinction I used to fumble: NFC isn't a rival to RFID, it's the short-range, 13.56 MHz corner of it—close range on purpose, so the card talks only to the thing you deliberately touch it to.)&lt;/p&gt;

&lt;p&gt;I'd always thought of NFC as a way to make the phone do something: open a link, pay for coffee, check in. But this flips it. The tap makes the card do something. For the half-second your phone is near it, the card is a live device—an inert rectangle of fiberglass that comes alive in your hand because you brought a battery near it. Once you've felt that, you start seeing NFC taps differently. You're not reading the tag. You're powering it.&lt;/p&gt;

&lt;p&gt;It's not a brand-new idea—U.S. Bank shipped a credit card a few years ago whose logo glows during a contactless payment, and it was popular enough that they ran short of them&lt;sup&gt;[3]&lt;/sup&gt;—but it's rare, and it feels like a small miracle every time. And it points at a whole category of objects: things that don't need a battery because they only need to be alive for the instant you're touching them with a phone you were already holding.&lt;/p&gt;

&lt;h2&gt;
  
  
  The card is a program
&lt;/h2&gt;

&lt;p&gt;I designed the whole board as code, which I'd recommend to anyone who, like me, finds dragging traces around with a mouse maddening. KiCad—the free PCB tool—has a Python API, so the entire card is generated by a script. Every trace, the antenna coil, the position of each letter: it's all computed. Want the coil to have one more turn? Change a number, re-run, done. The board is reproducible the way a program is reproducible, which for a hardware novice is enormously reassuring—there's no precious hand-drawn artifact I'm afraid to touch.&lt;/p&gt;

&lt;p&gt;I couldn't resist a couple of jokes for anyone who'd flip the card over. The back is laid out like a component datasheet—the dense reference document every hardware part ships with—complete with the actual resonance math for the antenna printed right there in the silkscreen, and two real exposed test points a curious engineer could probe with a meter to check my numbers. And on the front, where a chip card has its little gold square of contacts, mine has only a drawing of one—that same eight-pad footprint printed into the board with nothing underneath it. The real NFC chip is hidden on the back; the thing that looks like the chip is a decoy. Anyone who's laid out a circuit board clocks it instantly—everyone else just sees a chip card. It's there for whoever looks closely.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fww3mx7jdvvqvfg5m5udb.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fww3mx7jdvvqvfg5m5udb.webp" alt="Render of the black back of the card, laid out like a component datasheet: a gold " start="" width="800" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The back, straight from the design files—laid out like a real datasheet, resonance math and all, with TP1/TP2 you can actually probe. The Charizard's tail is where the LED lives.&lt;/p&gt;

&lt;p&gt;Then I sent it to a factory. This is the genuinely wild part of modern hardware: I uploaded my files to JLCPCB, and for about seventy-five dollars they manufactured ten copies and soldered the chip and the LED onto each one&lt;sup&gt;[4]&lt;/sup&gt;. Ten assembled circuit boards, in the mail, for the price of a nice dinner. A few days later, this little blue box showed up:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl6avfetsukfqnihruc5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcl6avfetsukfqnihruc5.webp" alt="A hand holding the blue JLCPCB shipping box that contained the assembled circuit-board business cards." width="800" height="1067"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inside were ten beautiful cards, and I was very pleased with myself. For about a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then I couldn't write to it
&lt;/h2&gt;

&lt;p&gt;I tapped the first card to my phone. The LED flickered—so it was getting power, the antenna worked, the hard electrical part was fine—but nothing happened. No link. The card had power but no message.&lt;/p&gt;

&lt;p&gt;That's expected: a blank chip needs you to write the URL onto it. So I opened NFC Tools, the standard app everyone uses for this, held the card to my iPhone, and got: "NFC tag not supported."&lt;/p&gt;

&lt;p&gt;I had made a rookie mistake in chip selection. The chip I'd picked, the NTAG I²C Plus&lt;sup&gt;[2]&lt;/sup&gt;, is not really a plain sticker tag. It's a bridge chip—designed to sit inside a gadget and talk to a microcontroller over a wire, with the NFC part as a bonus. It's the same chip that gives me the energy-harvesting pin I wanted for the LED, which is exactly why I chose it. But it ships blank, without the NDEF formatting that tells a consumer app where a URL record belongs. NFC Tools uses iOS's high-level NDEF path, which treated my blank chip as unsupported instead of formatting it.&lt;/p&gt;

&lt;p&gt;Fine, I thought. I'll try an Android instead—its NFC stack is far more permissive about this kind of thing. I dug out my backup phone, went to install the app, and discovered that my cheap prepaid Android doesn't have an NFC radio at all. The model officially supports NFC; my particular carrier's version of it had the chip left out to save a dollar. The phone literally cannot do NFC. There is no app for missing hardware.&lt;/p&gt;

&lt;p&gt;The ordinary iPhone app wouldn't; my Android couldn't. Ten gorgeous cards, and no way to put a single word on any of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing my own key
&lt;/h2&gt;

&lt;p&gt;The thing that got me unstuck was realizing the consumer apps weren't failing because the chip couldn't be written—they were stopping at the high-level NDEF check. Underneath, this chip answers the same basic memory commands as common NFC tags. It's command-compatible where it counts. The apps were bouncers checking IDs at the door; the chip inside would happily take my order.&lt;/p&gt;

&lt;p&gt;And iOS, it turns out, will let you send a tag raw commands—if you're willing to write the app yourself. Apple's Core NFC framework has a low-level mode where you talk to the tag directly instead of asking iOS to interpret it for you&lt;sup&gt;[5]&lt;/sup&gt;. So I wrote a very small iPhone app—about sixty lines that matter—that does exactly one thing: open an NFC session and send the write commands by hand. First the little header that declares "I'm a URL tag," then my web address, spelled out four bytes at a time across the chip's memory, each command acknowledged before the next.&lt;/p&gt;

&lt;p&gt;I built it onto my phone, opened it, tapped a card, and watched it count up the pages: writing, writing, done. Then I locked the phone, tapped the card again like a stranger would, and a banner popped up offering to open my site. It worked. It has worked on every card since.&lt;/p&gt;

&lt;p&gt;One quirk: I have to hold the card right against the top edge of the phone or the write fails. The coupling is tight, and I'm fairly sure the reason is that glowing LED—it's drinking some of the same power the chip needs to do its job, so there's less margin than a plain tag would have. A fun problem to have. It's on the list for the next revision: a little less glow, a little more range, and decide how I feel about that trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I keep thinking about
&lt;/h2&gt;

&lt;p&gt;What this drove home—again—is that the hard part of a hardware project is often the software at the very end: the last inch between "the object physically exists" and "the object does the thing." The electronics were textbook. The antenna math worked on the first try. What nearly beat me was a compatibility checkbox in an app that had never heard of my chip.&lt;/p&gt;

&lt;p&gt;But that's not what I think about when I hold the card. What I think about is the glow. It's a flat piece of fiberglass with no power source, and it comes alive because you waved a phone at it. I set out to make a business card that opens a website, which is a mildly clever party trick. I ended up more interested in the few milliwatts of borrowed light—because that same trick, the object waking up on power it doesn't own, is the seed of a hundred other things I now want to build.&lt;/p&gt;

&lt;p&gt;Anyway. If we meet and I hand you a black card, tap it. And watch the tail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Citations
&lt;/h2&gt;

&lt;p id="cite-1"&gt;[1] &lt;a href="https://en.wikipedia.org/wiki/Near-field_communication" rel="noopener noreferrer"&gt;Near-field communication&lt;/a&gt; — Wikipedia (NFC operates at 13.56 MHz) ↩&lt;/p&gt;

&lt;p id="cite-2"&gt;[2] &lt;a href="https://www.nxp.com/docs/en/data-sheet/NT3H2111_2211.pdf" rel="noopener noreferrer"&gt;NT3H2111/NT3H2211 NTAG I²C plus datasheet&lt;/a&gt; — NXP Semiconductors (energy harvesting output, ~5 mA typical) ↩&lt;/p&gt;

&lt;p id="cite-3"&gt;[3] &lt;a href="https://www.businesswire.com/news/home/20221019006081/en/U.S.-Bank-Shopper-Cash-Rewards%E2%84%A2-Visa-Signature%C2%AE-Card-slims-wallets-expenses" rel="noopener noreferrer"&gt;U.S. Bank Shopper Cash Rewards Visa launch&lt;/a&gt; — Business Wire (LED card that illuminates during contactless payment) ↩&lt;/p&gt;

&lt;p id="cite-4"&gt;[4] &lt;a href="https://jlcpcb.com/" rel="noopener noreferrer"&gt;JLCPCB&lt;/a&gt; — PCB fabrication and assembly service ↩&lt;/p&gt;

&lt;p id="cite-5"&gt;[5] &lt;a href="https://developer.apple.com/documentation/corenfc/nfcmifaretag/sendmifarecommand(commandpacket:completionhandler:)" rel="noopener noreferrer"&gt;sendMiFareCommand&lt;/a&gt; — Apple Developer documentation (sending native commands to an NFC tag) ↩&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>pcb</category>
      <category>nfc</category>
      <category>indiedev</category>
    </item>
    <item>
      <title>How to Jump to the Top or Bottom of Any App on macOS</title>
      <dc:creator>Kevin Tang</dc:creator>
      <pubDate>Sun, 06 Sep 2026 18:08:09 +0000</pubDate>
      <link>https://dev.to/kevintang/how-to-jump-to-the-top-or-bottom-of-any-app-on-macos-lc4</link>
      <guid>https://dev.to/kevintang/how-to-jump-to-the-top-or-bottom-of-any-app-on-macos-lc4</guid>
      <description>&lt;p&gt;I spend a lot of my day at the bottom of something. A long Slack channel, a 400-comment GitHub thread, a chat log, a diff that someone clearly generated with a for-loop. And constantly I want to get back to the top — or shoot down to the very bottom — and on a Mac there is no obvious, universal way to do it.&lt;/p&gt;

&lt;p&gt;You'd think there would be. iOS has it: tap the status bar, you're at the top. Every iPhone user knows that one. On the Mac? It depends entirely on which app you happen to be in, and the answer changes app to app in ways that have quietly annoyed me for years.&lt;/p&gt;

&lt;p&gt;So here's the actual state of jumping to the top or bottom on macOS — the shortcuts that exist, why they're such a mess, and what I eventually did about it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The keyboard shortcuts (such as they are)
&lt;/h2&gt;

&lt;p&gt;In a lot of apps, &lt;strong&gt;⌘↑ (Command + Up Arrow)&lt;/strong&gt; jumps to the top of the document and &lt;strong&gt;⌘↓&lt;/strong&gt; to the bottom. Safari, Chrome, Notes, most plain text views — this works. Great.&lt;/p&gt;

&lt;p&gt;Except:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In &lt;strong&gt;Finder&lt;/strong&gt;, ⌘↑ doesn't scroll to the top. It opens the &lt;em&gt;enclosing folder&lt;/em&gt;. Muscle memory from your browser will quietly send you up a directory.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Terminal and iTerm&lt;/strong&gt;, ⌘↑ scrolls to the previous command, not the top. To reach the very top of the scrollback you want &lt;strong&gt;⌘Home&lt;/strong&gt; — if your keyboard even has a Home key.&lt;/li&gt;
&lt;li&gt;Which is the other problem: &lt;strong&gt;Home and End&lt;/strong&gt;. On a full-size keyboard, Home jumps to the top of a field and End to the bottom, and they're great. But Apple stopped putting Home and End keys on laptops a long time ago. On a MacBook you fake them with &lt;strong&gt;Fn + Left Arrow&lt;/strong&gt; (Home) and &lt;strong&gt;Fn + Right Arrow&lt;/strong&gt; (End) — and even then, in many apps they move the &lt;em&gt;text cursor&lt;/em&gt; rather than the &lt;em&gt;scroll position&lt;/em&gt;, which is a different thing entirely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also &lt;strong&gt;Space&lt;/strong&gt; to page down and &lt;strong&gt;Shift + Space&lt;/strong&gt; to page up, which is genuinely useful and underused — but that's paging, not jumping, and you'll be mashing it a while to clear a long page.&lt;/p&gt;

&lt;p&gt;So the honest summary: there are roughly four different ways to scroll to the top of something on a Mac, none of them work everywhere, and the keys you'd most want to reach for don't exist on the keyboard you actually own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is it like this?
&lt;/h2&gt;

&lt;p&gt;I don't think anyone decided this on purpose. macOS never standardized "go to top / go to bottom" the way it standardized copy and paste — each app's developer wired up ⌘↑, Home, and the rest to whatever made sense in their app, and they all made slightly different, individually reasonable choices. Then laptops dropped the dedicated navigation keys to save space, and the whole thing calcified into the mess we have now. iOS, built later and from a blank page, got the clean version: tap the top, you're home.&lt;/p&gt;

&lt;p&gt;You adapt, of course. You learn that Finder is weird, you learn ⌘Home in the terminal, you reach for the scrollbar when in doubt. But "you adapt" is the sentence I tend to say right before I build something.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did about it
&lt;/h2&gt;

&lt;p&gt;I made a tiny menu-bar app called &lt;strong&gt;&lt;a href="https://kevintang.xyz/apps/scroll-elevator/" rel="noopener noreferrer"&gt;Scroll Elevator&lt;/a&gt;&lt;/strong&gt;. The idea is almost embarrassing in how direct it is: when you scroll, two little elevator buttons appear right at your cursor — one to jump to the top, one to the bottom. Click and the window leaps there. Hold one and the page glides, faster the longer you hold. Move your mouse away and they fade.&lt;/p&gt;

&lt;p&gt;What makes it click is that your hand never leaves the trackpad. To hit Home or ⌘↑ you reach for the keyboard and have to remember which incantation &lt;em&gt;this particular app&lt;/em&gt; wants; Scroll Elevator puts top and bottom under the cursor your hand is already on. And it behaves the same in Chrome, Finder, Preview, Mail, Xcode, and your terminal — because under the hood it drives each app's own scrollbar instead of firing a keystroke and hoping.&lt;/p&gt;

&lt;p&gt;It's $4.99, one time, signed and notarized, and nothing ever leaves your Mac. If you'd rather not pay, the whole thing is &lt;a href="https://github.com/NSEvent/scroll-elevator" rel="noopener noreferrer"&gt;open source&lt;/a&gt; and you can build it yourself for free — I mean that, the instructions are right there in the README.&lt;/p&gt;

&lt;p&gt;Is a dedicated app for "scroll to the top" overkill? Maybe. But I do it something like fifty times a day, and the friction was real enough that I'd rather have one button than a flowchart of keyboard shortcuts in my head. If you've ever pressed ⌘↑ in Finder and watched it jump up a folder instead, you know the exact feeling I'm talking about.&lt;/p&gt;

</description>
      <category>macos</category>
      <category>productivity</category>
      <category>scrollelevator</category>
      <category>keyboardshortcuts</category>
    </item>
    <item>
      <title>A Game Controller Is a Second Keyboard for Your Mac</title>
      <dc:creator>Kevin Tang</dc:creator>
      <pubDate>Sun, 06 Sep 2026 18:06:45 +0000</pubDate>
      <link>https://dev.to/kevintang/a-game-controller-is-a-second-keyboard-for-your-mac-408i</link>
      <guid>https://dev.to/kevintang/a-game-controller-is-a-second-keyboard-for-your-mac-408i</guid>
      <description>&lt;p&gt;TL;DR: a game controller is a wireless macro pad you may already own. &lt;a href="https://kevintang.xyz/apps/controller-keys?utm_source=kevin-md&amp;amp;utm_medium=post&amp;amp;utm_campaign=controllerkeys-202608" rel="noopener noreferrer"&gt;ControllerKeys&lt;/a&gt; maps its buttons, sticks, triggers, paddles, touchpads, and motion sensors—whichever ones the hardware actually has—to keyboard shortcuts, mouse input, macros, scripts, webhooks, OBS, and other Mac actions. It supports the familiar Xbox and PlayStation controllers, but also Nintendo, Steam, Apple TV, 8BitDo, and more than 300 third-party gamepads.&lt;/p&gt;

&lt;p&gt;People pay $150 for a Stream Deck: fifteen buttons, wired to the desk. Meanwhile there is probably a DualSense, Xbox controller, Joy-Con, or tiny 8BitDo pad in a drawer somewhere. It already has Bluetooth, controls shaped for muscle memory, and enough inputs to run a surprising amount of a Mac. That asymmetry bothered me enough that I built ControllerKeys.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd2ece71wnymuudxywshl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd2ece71wnymuudxywshl.webp" alt="The current ControllerKeys interface mapping a PS5 DualSense controller to keyboard shortcuts, mouse actions, layers, chords, and sequences" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The core mechanic is layers. Hold or toggle a trigger and every button can mean something different. ControllerKeys supports up to twelve layers per profile, and unmapped controls can fall through to the layer below. A controller is not just one small keyboard—it can be a timeline controller, presentation remote, media remote, and automation console without changing devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which game controllers work on a Mac?
&lt;/h2&gt;

&lt;p&gt;ControllerKeys has first-class layouts and hardware-specific handling for these controllers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Family&lt;/th&gt;
&lt;th&gt;Supported controllers and special hardware&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Xbox&lt;/td&gt;
&lt;td&gt;Xbox Series X|S, Xbox One, Xbox 360, and Xbox Elite Series 2—including all four rear paddles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PlayStation&lt;/td&gt;
&lt;td&gt;PS5 DualSense, DualSense Edge, and PS4 DualShock 4 v1/v2—including touchpad regions, gyro, lightbar, and Edge paddles&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nintendo&lt;/td&gt;
&lt;td&gt;Switch Pro Controller, a single Joy-Con, or paired left and right Joy-Cons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Valve&lt;/td&gt;
&lt;td&gt;Original Steam Controller—including both touchpads, gyro, grips, and haptics, without Steam running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apple&lt;/td&gt;
&lt;td&gt;Second-generation Siri Remote / Apple TV Remote—including clickpad cursor control, side buttons, and iPod-wheel-style edge scrolling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8BitDo&lt;/td&gt;
&lt;td&gt;Zero 2 and Micro with dedicated layouts and corrected raw d-pad input, plus Pro 2, SN30 Pro, SN30 Pro+, Ultimate, Lite, Lite 2, Lite SE, and many others&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is only the first-class list. Any standard controller recognized by macOS's GameController framework can work out of the box. If macOS does not expose a useful mapping, ControllerKeys can read the raw HID device and apply a bundled copy of the SDL GameControllerDB. The current bundle contains 314 macOS mappings, including models from Logitech, PowerA, Hori, Razer, SteelSeries, GameSir, Amazon Luna, Google Stadia, and many smaller brands.&lt;/p&gt;

&lt;p&gt;There is an important distinction here: a DualSense, Steam Controller, Siri Remote, or 8BitDo Micro gets device-specific handling for its unusual controls. A generic SDL fallback controller gets its standard buttons, sticks, and triggers mapped automatically. I would rather make that boundary clear than pretend every obscure gamepad has a gyroscope or touchpad it does not physically contain.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can a controller button do on macOS?
&lt;/h2&gt;

&lt;p&gt;A button can send a key or keyboard shortcut, hold a modifier, click or drag the mouse, type or paste text, launch an app, open a link, switch profiles, run a shell command or JavaScript, send an HTTP webhook, control OBS over WebSocket, or output a virtual MIDI control change. Macros chain those actions with delays.&lt;/p&gt;

&lt;p&gt;Tap, double-tap, long-hold, repeat, chords, and ordered button sequences are separate triggers. Profiles can switch automatically with the frontmost app or the connected controller. The sticks can control the pointer, scrolling, WASD, arrow keys, or eight custom directions. DualSense and DualShock 4 owners can also use the gyro as a mouse and split the touchpad into four independently mappable regions.&lt;/p&gt;

&lt;p&gt;That feature list sounds excessive until it is attached to a real workflow. These are the five where the controller has earned a permanent spot on my desk—or on somebody else's.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Video editing from the couch
&lt;/h2&gt;

&lt;p&gt;Map a stick to scrolling or bind its directions to J, K, L, and the arrow keys, depending on the editor. Put cut, trim, ripple delete, undo, and markers on the face buttons. Hold L2 for color controls and R2 for audio. A macro can handle the multi-step actions, such as exporting the current range.&lt;/p&gt;

&lt;p&gt;The advantage is not that a controller replaces every editing surface. It is that common timeline work becomes comfortable from a couch, standing desk, or any position where a keyboard full of memorized chords is awkward.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. OBS scenes without a Stream Deck
&lt;/h2&gt;

&lt;p&gt;ControllerKeys speaks OBS WebSocket directly and can import Stream Deck V2 profiles. Buttons, chords, and macros can switch scenes, toggle sources or filters, start or stop recording, and mute audio. The optional stream overlay shows controller input inside an OBS capture.&lt;/p&gt;

&lt;p&gt;The use case that surprised me was a physics professor at Arizona State running 200-student hybrid lectures with a DualSense. OBS scene switching goes on the face buttons, slides on the shoulders, and the touchpad becomes a cursor. The controller also has a laser-pointer overlay and a ready-made Google Slides / PowerPoint profile. Unlike a wired control deck, it can travel around a lecture hall.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Use an Xbox or PS5 controller as a Mac mouse
&lt;/h2&gt;

&lt;p&gt;For a Mac mini connected to a TV, one stick moves the mouse and the other scrolls. A trigger can slow the pointer for precise clicks. The on-screen keyboard supports d-pad navigation and touchpad swipe typing, while the app bar, media controls, command wheel, and directory navigator cover the things I would otherwise get up to find a keyboard for.&lt;/p&gt;

&lt;p&gt;ControllerKeys can also hand the controller-driven pointer, keyboard events, and mapped actions from one Mac to another over the local network or Tailscale. It is essentially Universal Control for the controller sitting in your hands.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Anki with a game controller
&lt;/h2&gt;

&lt;p&gt;Medical students found ControllerKeys on their own and became one of its clearest audiences. Put Again, Hard, Good, and Easy on four buttons and review cards without hovering over a keyboard. That matters when the same four actions happen thousands of times.&lt;/p&gt;

&lt;p&gt;I wrote the full setup up separately and included a prebuilt AnKing profile: &lt;a href="///anki-on-a-game-controller.md"&gt;Anki on a Game Controller&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The tiny 8BitDo Zero 2 and Micro are especially good here. ControllerKeys has dedicated layouts for both, including a raw-input fix for d-pads that macOS can misread.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Everyday Mac macros
&lt;/h2&gt;

&lt;p&gt;This is the boring one I use most. Window snapping on the d-pad, tap to paste a text snippet, double-tap for a shell command, long-hold to switch apps. Four face buttons already hold twelve actions before adding a chord or layer.&lt;/p&gt;

&lt;p&gt;The same system works for presentation controls, music production through virtual MIDI, smart-home HTTP requests, scripts that inspect the frontmost app, or app-specific shortcuts that activate only when Final Cut Pro, Photoshop, a browser, or a terminal is in front. Realtime mode skips the advanced-trigger delay for simple key mappings when latency matters.&lt;/p&gt;

&lt;p&gt;I wrote more about why controller chords stay usable when keyboard shortcuts do not in &lt;a href="///every-shortcut-within-reach.md"&gt;Every Shortcut Within Reach&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest caveats
&lt;/h2&gt;

&lt;p&gt;Bluetooth pairing is still Bluetooth pairing, and occasionally macOS will make that annoying. ControllerKeys also needs Accessibility permission to generate keyboard and mouse events; Automation permission is requested only for features that need it. The source is public for inspection, but it is source-available under PolyForm Noncommercial 1.0.0—not permissively licensed open-source software.&lt;/p&gt;

&lt;p&gt;There is a learning curve. The first hour with a blank profile is remapping, not productivity. Community profiles and setup guides make that easier, and configuration history lets you undo a bad import or destructive edit.&lt;/p&gt;

&lt;p&gt;ControllerKeys requires macOS 14.6 or later. The full app is free to try for 14 days with no account, then a one-time license is $19.99. Install it with Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; nsevent/tap/controllerkeys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or &lt;a href="https://github.com/NSEvent/xbox-controller-mapper/releases/latest" rel="noopener noreferrer"&gt;download the latest notarized DMG from GitHub&lt;/a&gt;. The &lt;a href="https://kevintang.xyz/apps/controller-keys?utm_source=kevin-md&amp;amp;utm_medium=post&amp;amp;utm_campaign=controllerkeys-202608" rel="noopener noreferrer"&gt;ControllerKeys website&lt;/a&gt; has videos, controller-specific guides, and the full feature list; the &lt;a href="https://github.com/NSEvent/xbox-controller-mapper" rel="noopener noreferrer"&gt;public source&lt;/a&gt; is available if you want to inspect it or build your own noncommercial copy.&lt;/p&gt;

</description>
      <category>controllerkeys</category>
      <category>macos</category>
      <category>gamecontrollers</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
