uv

Activity, over time

Daily observations from the public event archive.

Attention

Daily stars & forks

Loading attention chart

Star accumulation

Cumulative stars

Loading star accumulation chart

Pull requests

Daily opens & merges

Loading pull requests chart

Conversations

Daily issues & comments

Loading conversations chart

Community

Daily accounts & reviews

Loading community chart

Releases

Daily releases

Loading releases chart

Mentions

44posts · 30d
Nov 30 posts
  1. DEV · · @lu1tr0n

    Tinyworldformer corre en un Pico 2W de $6 con 520 KB de RAM Un microcontrolador de 6 dólares acaba de ejecutar, en tiempo real, un modelo de mundo comprimido que inventa videojuegos cuadro por cuadro. El proyecto tinyworldformer corre dentro de 520 KB de RAM en una Raspberry Pi Pico 2W, sin GPU, sin nube y sin conexión a internet. Su autor lo logró separando el trabajo en dos redes neuronales por fotograma. Una genera el siguiente cuadro del juego, la otra decide qué botón presionar, y ambas corren a la vez sobre el mismo chip. TL;DR - Int8 y pesos compartidos reducen el generador de frames a entre 26.000 y 131.000 parámetros según el juego.- Un bloque transformer aplicado de 1 a 4 veces sustituye a una pila de capas distintas y ahorra memoria.- La red actora, separada del generador, pesa solo entre 9.000 y 16.000 parámetros en float32.- El RP2350 reparte el trabajo: el núcleo 1 corre ambas redes, el núcleo 0 atiende USB y pantalla.- El repositorio SheepSeb/tinyworldformer deja jugar los cinco juegos reales desde la terminal con uv. ¿Qué es un modelo de mundo comprimido? Un modelo de mundo comprimido es una red neuronal generativa reducida en tamaño y precisión para correr sin GPU: tinyworldformer la implementa como un transformer en bucle cuantizado a 8 bits que predice el siguiente fotograma de un videojuego y cabe en los 520 KB de RAM de un microcontrolador de 6 dólares. La idea de partida de su autor fue simple. Los transformers en bucle reutilizan el mismo bloque de pesos varias veces en lugar de apilar capas distintas, así que ocupan mucha menos memoria por diseño. Si ese ahorro era suficiente, un modelo generativo completo podía caber en un chip de bolsillo. Por qué importa La mayoría de los modelos generativos de video o de juegos dependen de una GPU con varios gigabytes de memoria. Tinyworldformer invierte esa ecuación: corre en un chip pensado para termostatos y drones de juguete. Eso importa para el campo conocido como edge computing, donde el objetivo es mover la inferencia al dispositivo en lugar de depender de un servidor remoto. Un modelo que corre en el borde no necesita conexión a internet, no paga latencia de red y no expone datos a un servidor externo. Acá el beneficio es además una prueba de concepto. Si una red generativa cabe en 520 KB, la pregunta deja de ser si el edge AI puede generar contenido y pasa a ser cuánto contenido puede generar con ese presupuesto de memoria. El ángulo de costo pesa tanto como el técnico. Una Raspberry Pi Pico 2W cuesta una fracción de lo que cuesta cualquier GPU capaz de correr un transformer completo, y consume órdenes de magnitud menos energía porque no tiene memoria de video ni refrigeración activa. Para proyectos de hobby, educación o investigación en TinyML, esa diferencia de costo decide si un experimento se hace o no. Cómo funciona tinyworldformer por dentro Tinyworldformer separa el problema en dos redes con trabajos distintos. La primera, el generador de frames, recibe el cuadro actual y la acción del jugador y predice el cuadro siguiente junto con la recompensa. La segunda, la red actora, solo mira el cuadro y decide qué acción tomar. Ninguna de las dos ve jamás el juego real una vez que arranca la sesión: el generador solo recibe acciones de la actora, y la actora solo ve cuadros inventados por el generador. sequenceDiagram participant Actor as Red actora participant Generador as Generador de frames Actor->>Generador: envia accion Generador-->>Actor: entrega frame generado y recompensa Note over Actor,Generador: el generador nunca ve el juego real Tokenización en parches Antes de entrar al transformer, cada cuadro se recorta en parches cuadrados: 2x2 celdas en Breakout, 4x4 en Doom-lite, Flappy Bird y Pong, 6x6 en Pac-Man lite. Eso da entre 36 y 80 tokens por juego según el repositorio. Cada token suma un vector aprendido por celda, con una tabla distinta según la posición dentro del parche para no perder el orden, más un vector de posición y un vector de la acción tomada. Un ejemplo simplificado en Python muestra la misma idea con un cuadro diminuto de 4x4 dividido en parches de 2x2: import numpy as np frame = np.array([ [1, 1, 2, 2], [1, 1, 2, 2], [3, 3, 4, 4], [3, 3, 4, 4], ]) def a_parches(frame, parche=2): h, w = frame.shape tokens = [] for i in range(0, h, parche): for j in range(0, w, parche): tokens.append(frame[i:i+parche, j:j+parche].flatten()) return np.array(tokens) print(a_parches(frame)) La salida literal es una matriz de 4 tokens, uno por cada región homogénea del cuadro: [[1 1 1 1] [2 2 2 2] [3 3 3 3] [4 4 4 4]] El transformer en bucle En lugar de apilar, por ejemplo, ocho bloques transformer distintos, tinyworldformer usa un único bloque (atención más una red feedforward con ReLU) y lo aplica entre 1 y 4 veces seguidas, sumando la entrada original antes de cada pasada. El resultado de cada pasada se entrena contra el cuadro siguiente real, así que el número de pasadas en tiempo de ejecución es un control directo entre velocidad y precisión. En el Pico 2W, para mantener una tasa de cuadros razonable, el proyecto usa dos pasadas. flowchart TD A["Frame t + accion"] --> B["Tokenizar en parches"] B --> C["Bloque transformer (pesos compartidos)"] C --> D{"Repetir de 1 a 4 veces"} D -->|"otra pasada"| C D -->|"pasada final"| E["Frame t+1 y recompensa"] Cuantización y reparto entre núcleos El generador de frames se cuantiza a int8, mientras que la red actora se queda en float32 porque ya es pequeña de por sí, entre 9.000 y 16.000 parámetros, y la precisión ahí importa más para decidir bien la acción. El RP2350 del Pico 2W tiene dos núcleos: el núcleo 1 corre el generador y la actora, el núcleo 0 se encarga del USB y de la pantalla, según documenta el repositorio. Esa separación entre generador y actor permite optimizar cada red por separado. El generador, la parte pesada, recibe toda la cuantización, mientras que la actora, ya liviana, se queda intacta en float32. El RP2350 integra FPU, pieza clave para ejecutar redes cuantizadas. flowchart TD A["RP2350 dual-core"] --> B["Nucleo 0: USB y pantalla"] A --> C["Nucleo 1: inferencia"] subgraph Inferencia["Nucleo de inferencia"] C --> D["Generador de frames int8"] C --> E["Red actora float32"] end Ejemplos prácticos y cómo empezar El repositorio incluye cinco juegos reales escritos en unas pocas líneas de numpy, lo que le permite a su autor comparar cada frame generado contra la física real del juego. La siguiente tabla resume el costo de memoria y cómputo de cada uno, con datos publicados en el repositorio de tinyworldformer: JuegoTamaño de frameParámetrosMACs por frameFrames exactosBreakout20x1626k4.3M99.99%Doom-lite32x3255k4.2M83.6%Flappy Bird24x2446k2.0M100%Pong20x3246k2.3M99.45%Pac-Man lite48x48131k8.2M86.4% La columna de frames exactos mide el porcentaje de transiciones donde cada celda del cuadro predicho coincide con el cuadro real. Doom-lite tiene el número más bajo, 83.6%, pero no porque juegue mal: su vista en primera persona tiene 768 celdas, y un solo píxel de sombra mal pintado ya marca todo el cuadro como incorrecto en esta métrica. Cómo probarlo en tu máquina El proyecto usa uv como gestor de paquetes de Python, así que no hace falta instalar dependencias a mano una por una. En Linux o macOS: curl -LsSf https://astral.sh/uv/install.sh | sh git clone https://github.com/SheepSeb/tinyworldformer cd tinyworldformer uv run python -m tinyworldformer.games.play --game pacman En Windows, el único paso que cambia es la instalación de uv, desde PowerShell: powershell -c "irm https://astral.sh/uv/install.ps1 | iex", y luego los mismos comandos de git clone y uv run. El comando play acepta también breakout, doomlite, flappy o pong en lugar de pacman. No hay salida de texto que mostrar: uv descarga las dependencias declaradas en el pyproject.toml y abre una ventana gráfica con el juego real corriendo en numpy, el mismo que el generador de frames aprendió a imitar. > 💡 Tip: correr primero el juego real con play antes de buscar los pesos entrenados ayuda a entender qué está tratando de imitar el generador cuantizado, celda por celda. Casos de uso reales Tinyworldformer no apunta a reemplazar un motor de videojuegos. Su valor está en lo que demuestra: un modelo generativo entrenado sobre partidas grabadas puede, cuantizado y comprimido, correr en el mismo chip que hoy maneja sensores de temperatura o drones de juguete baratos. El caso de uso inmediato es educativo y de investigación en TinyML, pero la receta (tokenizar en parches, compartir pesos entre pasadas, cuantizar solo la parte más pesada) es trasladable a otros generadores pequeños, como un simulador de sensor para probar un controlador embebido sin hardware real conectado. El repositorio también suma un agente de refuerzo por streaming que aprende a jugar dentro del sueño generado por el propio modelo, en lugar de solo repetir una política ya entrenada. Cualquier botón del dispositivo le devuelve el control al usuario humano, así que la misma placa sirve para ver a la IA jugar sola o para jugar uno mismo dentro del mundo que ella inventa. Breakout necesita solo 26.000 parámetros; Pac-Man lite necesita 131.000. Errores comunes y buenas prácticas - Confundir precisión de píxel con jugabilidad. Un 83.6% de frames exactos suena mal hasta que se entiende la métrica: un solo píxel de sombra distinto ya cuenta como fallo total del cuadro, aunque el estado del juego (posición del enemigo, vida, puntaje) siga siendo correcto.- Cuantizar todo por igual. Aplicar int8 a la red actora, que ya tiene menos de 16.000 parámetros, aportaría poco ahorro de memoria y arriesgaría la precisión de la política. La cuantización rinde donde el modelo es grande, no donde ya es pequeño.- Elegir mal el número de pasadas del bucle. Una sola pasada del transformer es más rápida pero deja artefactos visibles, un enemigo que no debería estar ahí, por ejemplo. Cuatro pasadas corrigen casi todo el error pero cuestan más tiempo de cómputo por cuadro. Dos pasadas fue el punto que eligió el proyecto para el Pico 2W.- Verificar contra la física real, no solo mirar el video generado. Tener la versión real del juego en numpy, como hace este repositorio, es lo que permite medir el error cuadro por cuadro en lugar de confiar en que se vea bien. Comparativa con alternativas para generar contenido con IA EnfoqueCuándo usarloVentajaLimitaciónTransformer completo en GPU o nubeCuando la latencia de red no importa y hay presupuesto de cómputoMáxima fidelidad del frame generadoDepende de conexión e infraestructura externaModelo de mundo comprimido en microcontrolador (tinyworldformer)Dispositivos aislados, sin GPU, con RAM y flash muy limitadosCorre offline, sin latencia de red, consumo mínimoSolo viable en juegos o dominios muy pequeñosDistillation genérica a modelos pequeños (TinyML clásico)Tareas de clasificación o detección simples en sensoresHerramientas y runtimes madurosNo pensado para generar secuencias completas de frames La fila intermedia describe a tinyworldformer. No busca competir en fidelidad visual con un generador de video entrenado en GPU; busca que el juego siga siendo jugable dentro de un presupuesto de memoria minúsculo. Profundizando: por qué el bucle ahorra memoria y dónde se nota el límite La razón de fondo por la que un transformer en bucle pesa menos es aritmética simple: un transformer tradicional de N capas guarda N juegos de pesos distintos, mientras que el bucle guarda un solo juego de pesos y lo reusa N veces. El costo que se paga a cambio es tiempo de cómputo, cada pasada adicional cuesta los mismos multiplica-acumula que la anterior, en lugar de memoria, y en un microcontrolador la memoria suele ser el recurso más escaso de los dos. La cuantización int8 agrega un segundo ahorro, independiente del anterior: cada peso pasa de ocupar 4 bytes en float32 a ocupar 1 byte, a costa de redondear cada valor a uno de 256 niveles posibles. El siguiente ejemplo en Python muestra ese redondeo con cuatro pesos cualquiera: import numpy as np pesos = np.array([0.42, -0.87, 1.35, -1.90], dtype=np.float32) escala = np.max(np.abs(pesos)) / 127 cuantizados = np.round(pesos / escala).astype(np.int8) reconstruidos = cuantizados.astype(np.float32) * escala print(cuantizados) print(np.round(reconstruidos, 4)) La salida literal es: [ 28 -58 90 -127] [ 0.4189 -0.8678 1.3465 -1.9 ] El peso original de 1.90 se reconstruye exacto porque definió la escala; los otros tres cargan un pequeño error de redondeo. Ese error, multiplicado por miles de pesos y varias pasadas del bucle, ayuda a explicar por qué Pac-Man lite, el juego más complejo con 131.000 parámetros, tiene un porcentaje de frames exactos menor que Flappy Bird. No hay una forma directa de verificar desde afuera, sin acceso al checkpoint, cuánto de ese error viene de la cuantización y cuánto del tamaño del juego; lo más cercano que ofrece el propio repositorio es comparar el porcentaje de frames exactos por juego de la tabla anterior. El FPU del RP2350 importa en este punto. Sin una unidad de punto flotante en hardware, la dequantización, volver de int8 a valores reales antes de cada operación de atención, sería mucho más lenta, porque el chip tendría que emular esas operaciones en software. > 💭 Clave: el ahorro de memoria del bucle y el ahorro de la cuantización son independientes entre sí; tinyworldformer los combina, pero cada uno podría usarse por separado en otro proyecto. 📖 Resumen en Telegram: Ver resumen Tu próximo paso: cloná el repositorio, corré uv run python -m tinyworldformer.games.play --game breakout y leé el archivo de ese juego dentro de src/tinyworldformer/games/ para ver cuántas líneas de numpy hacen falta para una física jugable completa. Preguntas frecuentes ¿Qué microcontrolador corre el modelo de mundo comprimido de tinyworldformer? Una Raspberry Pi Pico 2W, construida sobre el chip RP2350, que tiene dos núcleos y una unidad de punto flotante. El núcleo 1 ejecuta el generador de frames y la red actora; el núcleo 0 atiende USB y la pantalla. ¿Cuántos parámetros tiene el transformer en bucle de cada juego? Entre 26.000 y 131.000, según la complejidad del juego: Breakout es el más liviano y Pac-Man lite el más pesado, de acuerdo con la tabla publicada en el repositorio. ¿Hace falta GPU para entrenar tinyworldformer antes de cuantizarlo? El repositorio no detalla el hardware exacto de entrenamiento, pero el generador de frames se entrena sobre partidas grabadas del juego real, un proceso que normalmente se hace fuera del microcontrolador y antes de cuantizar los pesos para desplegarlos en el Pico 2W. ¿Qué diferencia a la red actora de la IA embebida en microcontroladores que genera los frames? La red actora es una MLP mucho más chica, entre 9.000 y 16.000 parámetros, que se queda en float32 porque su tarea, decidir una acción a partir de un frame, tolera peor el error de cuantización que generar el frame en sí. ¿Se puede correr tinyworldformer en un microcontrolador distinto al Pico 2W? El repositorio está construido específicamente para el RP2350 del Pico 2W, incluyendo una carpeta en Rust para el firmware del dispositivo. Portarlo a otro chip exigiría al menos una FPU y memoria suficiente para los buffers de atención, pero el propio proyecto no documenta otros objetivos probados. Referencias - GitHub: SheepSeb/tinyworldformer: repositorio original con el código, los juegos en numpy y los datos de tamaño y precisión de cada modelo.- Raspberry Pi: fabricante oficial de la Raspberry Pi Pico 2W y el chip RP2350 usado en el proyecto.- Wikipedia: Edge computing: contexto sobre mover la inferencia de IA al dispositivo en lugar de a un servidor remoto.- Wikipedia: Quantization: fundamento matemático de la cuantización usada para pasar los pesos de float32 a int8. 📱 ¿Te gusta este contenido? Únete a nuestro canal de Telegram @programacion donde publicamos a diario lo más relevante de tecnología, IA y desarrollo. Resúmenes rápidos, contenido fresco todos los días.

  2. DEV · · @alanwest

    Python's JIT Is Being Removed After a 7-8% Gain. Here's What Makes Python Faster CPython's experimental JIT is being removed. It shipped in three releases (3.13, 3.14, 3.15), got a rewritten tracing frontend, register allocation and an LLVM 21 build dependency, and the official number on x86-64 Linux is a 7-8% geometric mean speedup. On October 19, 2026 the authors withdrew PEP 836, and the JIT code is expected to leave main early in the 3.16 cycle. My take: good. The JIT was never going to fix slow Python, and the time teams spent waiting for it would have been better spent on things that already work. If you were counting on it, here's how it got pulled, what it means for PYTHON_JIT and your builds, and what I'd use instead. How the CPython JIT ended up on the way out The short version, from LWN's write-up: - Brandt Bucher merged the copy-and-patch JIT for 3.13 in early 2024 (PR #113465) without a PEP. PEP 744 came afterwards, and it was Informational, not Standards Track. - On June 5, 2026 the Steering Council froze new JIT work on main. Bug and security fixes were still allowed. The council gave six months for a Standards Track PEP to be submitted and resolved. If none was accepted, "the JIT code must be removed from the main branch." - On July 3 Bucher, Savannah Ostrowski and Ken Jin posted PEP 836, "JIT Go Brrr". - On September 21 the council replied with a ten-point list covering things like memory impact, platform parity, user impact, security ("an underserved area in the PEP"), and a compatibility review the PEP scheduled for the first 3.17 release candidate, around August 2028. The council called that "way, way too late." - On October 19 Ostrowski withdrew the PEP. Per LWN: "we don't feel we have the personal bandwidth to carry the current version forward." So on paper this was a process decision. The JIT was paused, the authors were asked for a plan they could commit to, and they decided they couldn't carry it. The performance numbers are why nobody fought very hard to keep it. The JIT's numbers never matched the hype Here are the numbers the JIT's own docs and PEP published: | Release | Official JIT claim | Source | |---|---|---| | 3.14 | "can range from 10% slower to 20% faster, depending on workload" | What's New 3.14 | | 3.15 | 7-8% geomean on x86-64 Linux; 11-12% on AArch64 macOS vs the tail-calling interpreter | What's New 3.15 | | 3.15, per benchmark | "roughly 15% slowdown to over 100% speedup" | same | | PEP 836 runs, June 2026 | 1.047x (Windows, Ryzen 5 3600X) to 1.126x (M3 Pro macOS) | PEP 836 | That 3.15 per-benchmark range means some benchmarks run 15% slower with the JIT on. If you run a web service, the geomean doesn't tell you much; you get whatever your own code paths get. PEP 836 set its own target, and it asked for at least a 20% geomean improvement on pyperformance for JIT plus free-threading over the plain free-threaded interpreter, by the first 3.17 beta. The 3.15 number on the default x86-64 build was roughly a third of that. Different baseline, I know, but that's a big gap. For comparison, the 3.14 tail-calling interpreter showed 3-5% on pyperformance in preliminary benchmarks, just from changing how the interpreter dispatches opcodes. It needs a recent compiler (Clang 19 at the time), but no new compiler tier inside CPython and no new block of runtime-generated machine code to secure. The people who built it did serious work in 3.15: a trace-recording frontend, reference-count elimination, GDB unwinding through JIT frames. The problem is Python itself. As matklad pointed out on Lobsters, in Python "a lot of code that's being executed is the C code of the interpreter itself", and a bytecode JIT doesn't speed that up. It doesn't touch the C extensions most real programs lean on. Antonio Cuni, who has years of PyPy behind him, was blunter in the same thread: writing a Python implementation that is "real fast" and backwards compatible is "de facto impossible". He also says PyPy proves Python itself is optimizable, so his complaint is about compatibility, not the language. What happens to PYTHON_JIT and your builds - On 3.13, 3.14 and 3.15 nothing changes. LWN reports the council said it was OK with the status quo for 3.15. The official macOS and Windows binaries ship with the JIT built but off, and PYTHON_JIT=1 turns it on. - If you build from source, --enable-experimental-jit (and =yes-off for "built but disabled") still works on those branches. You still need LLVM at build time; for 3.15 that's LLVM 21. - From 3.16 on, the code is expected to be removed from main early in the cycle, per LWN. Plan for the configure flag and the env var to do nothing, or to disappear. - The code itself isn't going away entirely. Ostrowski said the reference implementation will be published and linked from the withdrawn PEP. Brett Cannon said work stops in the main repo; whether it continues elsewhere is up to the community. Check whether a given interpreter has it: import sys jit = getattr(sys, "_jit", None) if jit is None: print("no sys._jit here (Python < 3.14)") else: print("available:", jit.is_available(), "enabled:", jit.is_enabled()) sys._jit was added in 3.14 and has a leading underscore, so it won't exist on 3.13 and probably won't survive 3.16. Don't build anything on it. What actually makes Python faster in 2026 None of this is new, and all of it is less exciting than "Python gets a JIT". Profile, then fix the algorithm 3.15 ships the Tachyon sampling profiler (PEP 799) as profiling.sampling, and the docs claim sampling rates "up to 1,000,000 Hz" with virtually zero overhead. On older versions, use py-spy. In my experience the hot spot is usually a quadratic loop or an N+1 query, and no JIT would have saved you from either. Compile the hot module with mypyc, Cython or Numba In the HN thread, one commenter reported a fib benchmark dropping from 1.45s to 0.064s with mypyc. If your code already has type hints, all mypyc costs you is a build step. For numeric loops, Numba is still the practical answer. Move the hot core to Rust with PyO3 PyO3 and maturin made this boring, and boring is good. pydantic-core, Polars and orjson all keep Python at the surface and do the heavy lifting in Rust. Free-threading for CPU-bound parallel work 3.14 made the free-threaded build officially supported, with a single-threaded penalty of roughly 5-10%. For CPU-bound work on threads, that beats the JIT's single-digit gain because it scales with cores. (In 3.14 you couldn't have both anyway: free-threaded builds don't support the JIT.) 3.15 adds an abi3t stable ABI for free-threaded extensions, which should shorten the "is my C dependency safe" wait. Try it: uv python install 3.14t uv run --python 3.14t python -c "import sys; print(sys._is_gil_enabled())" False means the GIL is really off. If an extension that doesn't declare free-threading support gets imported, CPython turns the GIL back on and warns you. Startup time and tooling 3.15's explicit lazy import (PEP 810) and -X lazy_imports=all matter more for CLIs than any JIT ever would. On the tooling side, uv's README claims it is "10-100x faster" than pip, which is the kind of speedup your CI pipeline will actually notice. Bytecode dispatch was never what made it slow. PyPy or GraalPy, if you can Both show bigger speedups are possible, but HN commenters point out PyPy's newest supported version is 3.12 and loading arbitrary C extensions is still the weak spot. Checklist if you were counting on the JIT 1. Search CI configs, Dockerfiles and env files for PYTHON_JIT and --enable-experimental-jit. Keep them on 3.13-3.15 if you measured a win, and mark them for removal on 3.16. 2. If you build CPython with the JIT, drop the LLVM build dependency when you move to 3.16. 3. Rerun your own benchmarks with PYTHON_JIT=0. If the difference is under your normal noise, delete the setting today. 4. Profile production with py-spy or Tachyon and find your top three hot functions. 5. For each one, decide: algorithm fix, mypyc/Cython/Numba, or a Rust extension through PyO3. 6. If you have CPU-bound threading work, test the 3.14t build and check your C extensions for free-threading support. 7. Remove any sys._jit calls from code that has to run on 3.16. Was the JIT a waste of time? I don't think the code was wasted. The register allocation and unwinding work taught the core devs something, and the reference implementation will stay public. Where it did waste time was for teams who treated "the JIT is coming" as a reason not to profile, not to move hot code out of Python, or not to try free-threading. Someone on Lobsters called the council's process "death by bureaucracy". Someone on HN called it "a pretty stellar example of non-bureaucratic governance". I'm with the second one. Pablo Galindo Salgado, who posted the announcement for the council, followed up that the six months was "a first goal to anchor, not a bureaucratic deadline we will hold", and that an extension could be requested. Writing down a maintenance plan is not a big ask for a whole new compiler tier. So where do you stand? Should the core devs have kept going until they hit that 20% bar, or was a JIT always a dead end for CPython? And what actually made your Python faster: an algorithm fix, Rust, free-threading, or leaving Python? Sources - LWN: Python's experimental JIT to be removed - Steering Council announcement, June 2026 - PEP 836 withdrawal post - PEP 836 and PEP 744 - What's New in Python 3.14 - What's New in Python 3.15 - uv README - Discussion: Hacker News, Lobsters

  3. Graphify passed 125,000 GitHub stars Graphify has passed 125,000 GitHub stars on Graphify-Labs/graphify. On 12 October 2026 the public count stood at 125,736. The repository has also been forked more than 12,000 times. !Star history for Graphify-Labs/graphify, climbing from the first weeks in April 2026 to past 120,000 GitHub stars in October. Star history for Graphify-Labs/graphify, charted by star-history.com. The first commit was 5 April. We wrote about 100,000 GitHub stars in August, 117 days after that commit, and last week about passing 8 million PyPI downloads. The chart is that same stretch: a steep April, then a steady climb through October. People star the repository when the command is useful. Type /graphify in your assistant and a map of the codebase comes back in about thirty seconds, parsed on the machine with tree-sitter. That is the check. The star count is what shows up afterward. 125,000 GitHub stars The principles from the first weekend are the same ones in the 100,000-star post. - Code is parsed on your machine, from the files in the repository. - Every relationship is tagged EXTRACTED or INFERRED, so you can see what was read from the source and what Graphify resolved. - The result is a graph you can query and walk, from one function to the next. - Install is two commands. The run is one. The project is Apache 2.0. That stays. Work this sits on - tree-sitter turns source into the syntax tree that every code edge starts from. - Leiden community detection is how a repository splits into the subsystems on the graph. - Model Context Protocol is how an assistant calls that graph. - uv is why the install is short. Claude Code, Cursor, Codex, Gemini CLI, and Copilot are the assistants people run this in. The map is the part we maintain. To everyone who starred the repository, opened an issue, sent a pull request, answered a question, or wrote about Graphify: thank you. The open-source project stays open source. Graphify Cloud is the always-on version of the same map, kept current for the code, docs, and meetings you already work in. Thanks for the stars. Syed Fahad and the Graphify team

  4. YouTube · · YouTube description · Sidequest Dev

    Rust Didn't Win Because It's Fast GitHub rewrote Copilot's runtime in Rust. OpenAI's Codex is now almost all Rust. Bun, the runtime Claude Code ships on, moved 535,496 lines of Zig to Rust in 11 days. But it isn't about speed: C has been fast for half a century. We fly inside a real laptop, down to the CPU die and the memory cells, to show what Rust actually changed, and why AI coding agents love it. 0:00 Everyone is rewriting in Rust 0:49 This is Sidequest Explained 1:00 Inside the machine: instructions, bytes, stack and heap 1:53 Way one: freeing memory by hand (C and C++) 2:23 An elevator crash started Rust 2:40 Way two: the garbage collector (Discord's spikes) 3:12 Way three: ownership 3:36 Checked before it runs, and Android's results 4:07 Close to the metal: 28 bytes vs 8 4:21 Copilot: 5.25 s to 292 ms 4:41 Why AI agents love the Rust compiler 5:11 Is Python dead? What it means for you Sources GitHub Blog, migrating the Copilot runtime to Rust (Sep 2026): github.blog Bun, Rewriting Bun in Rust (Jul 2026): bun.sh OpenAI Codex repository: openai/codex Codex native rewrite discussion: codex#1174 Grok Build repository: xai-org/grok-build Dennis Ritchie, The Development of the C Language: bell-labs.com Microsoft MSRC, 70% of security bugs are memory safety (2019): microsoft.com Chromium memory safety: chromium.org MIT Technology Review, Rust's origin (2023): technologyreview.com Rust 1.0 announcement: blog.rust-lang.org Discord, Why Discord is switching from Go to Rust (2020): discord.com The Rust Book, ownership and borrowing: doc.rust-lang.org Google, memory-safe languages in Android 13 (2022): security.googleblog.com Google, Rust in Android (Nov 2025): blog.google Python sys.getsizeof / CPython ints: docs.python.org Hugging Face tokenizers: huggingface/tokenizers OpenAI tiktoken: openai/tiktoken pydantic-core: pydantic/pydantic-core uv by Astral: astral-sh/uv Numbers are the sources' own. The Copilot timings are client + session + one-turn overhead in the opt-in in-process mode, not counting the model or network. Android's 1000× is memory-safety vulnerability density. Code on screen is an illustration, not a run. Credits Die micrographs: Fritzchens Fritz (CC0). Memory-cell micrograph (Micron MT4C1024) and SEM plate (1886VE10): ZeptoBars, CC BY 3.0. Office and hardware photos: Pexels. HDRIs and materials: Poly Haven, ambientCG (CC0). Music: "Lofi Study" by FASSounds (Pixabay). Rust, GitHub, Copilot, OpenAI, Codex, Bun, Claude, xAI, Grok, Microsoft, Chrome, Android, Discord, Python, Hugging Face, Astral, pydantic and other names and logos belong to their owners, used for identification and commentary; Sidequest is not affiliated with any of them. Narration: an AI clone of my own voice. Animation: original, built in three.js. #rust #programming #ai

    50 likes · 14 comments · 6,610 views
  5. Real-looking people in a product video, for $0, on a MacBook: I tested 5 methods > TL;DR > - I made the same 30-second story five ways to find the best free, local way to put real-looking people in a product video: free stock, AI images (SDXL), AI video (LTX-Video), Blender 3D and a lip-synced AI narrator. > - Everything ran on one MacBook Pro (M1 Max, 64 GB), cost $0, and used about 28 GB of models. > - The winner is AI images with SDXL: realistic, consistent characters at ~2 min per shot on the Mac GPU. > - AI video (LTX-Video) is the only method where people truly move, but it took 11–26 min per 5-second clip, and crowds and big motion melt. > - Surprise: the video model generates a clip in ~30 s. The other 95% of the time is decoding plus memory swap. > - Everything is open: live side-by-side dashboard · repo with inputs, outputs, scripts and logs · full report !Same story, five ways. Rows: free stock, AI images, AI video, Blender 3D, talking narrator Same 30-second story, five ways. Rows top to bottom: free stock · AI images (SDXL) · AI video (LTX-Video) · Blender 3D · talking narrator (SadTalker). Columns are the five shots. ▶ Watch all five videos in sync: side-by-side dashboard · each video embedded on the original post Who this is for You make product demos, explainers or launch videos with code, an editor or a tool like Remotion or HyperFrames. Your app screens look great, but there are no humans: no customer, no user, no story. You want people in the video, you don't want to pay for footage or an AI video subscription, and you have a decent Mac. By the end you'll know: - which of the five free methods to use for which shot; - what each one really costs in time on Apple Silicon, and why; - the exact settings, prompts and patches that made them work, including the errors you'll hit and their fixes. The problem My explainer videos had a recurring hole. The product screens were sharp, but every "real world" moment (a candidate arriving, a phone buzzing, an interview) was a silhouette or an icon. Viewers connect with people. The usual answers are paid stock footage, a paid AI video tool, or filming. I wanted to know how far free and local gets you in 2026. How I tested (methodology) One story, five methods; only the people change. The story: Aarav gets a QR call letter for a walk-in interview → scans in at reception → waits in a lounge until his phone buzzes → interviews → his record is handed off to the company's HR system. That's 5 shots, 30.4 seconds in total. Fairness controls. Every version shares: - the same storyboard and shot timings (6.5 / 5.3 / 6.5 / 5.7 / 6.4 s); - the same narration (Kokoro TTS, voice am_michael, generated locally); - the same app overlay cards and the same music bed; - the same assembly template, rendered with HyperFrames (HTML + GSAP → MP4) at 1080p. Hardware. MacBook Pro, Apple M1 Max: 10-core CPU (8 performance + 2 efficiency), 32-core GPU, 64 GB of unified memory, macOS 26.5. There's no NVIDIA GPU, so "GPU" means the Apple GPU through PyTorch's mps backend or Blender's Metal renderer. I left my normal apps open (an IDE, a Linux VM, an editor), which turned out to matter. What I measured. - Wall-clock time per step, from logs. - Which device each step ran on. - Memory and swap. - Seven quality criteria scored 1–5: realism, character consistency, motion, story fit, speed, reliability and licence clarity. Prerequisites | You need | Why | |---|---| | Apple Silicon Mac, ideally 32 GB+ of RAM | The AI video model needs ~25 GB of working memory in fp32 | | ~30 GB of free disk | Model weights | | Python 3.10 and 3.11, uv, ffmpeg | Two separate environments; SadTalker needs old pinned libraries | | Blender 4.5 | Method 4. Use the blender.org DMG; the Homebrew cask asks for sudo | | Node 18+ | npx hyperframes for the final assembly (any editor works) | The models and their download sizes: | Model | Used for | Size | Licence | |---|---|---|---| | RealVisXL V4 (SDXL, fp16) + fp16-fix VAE | AI images | 7.3 GB | Open RAIL++-M | | LTX-Video 0.9.5 transformer + VAE | AI video | 6.3 GB | Open RAIL-M | | T5-XXL encoder, bf16 copy | LTX text encoder | 9.5 GB (instead of 19 GB) | Apache-2.0 | | SadTalker + GFPGAN weights | Narrator | 1.7 GB | Apache-2.0 | | Quaternius animated characters | Blender | small | CC0 | Method 1: free stock photos How it works. Search Wikimedia Commons (its API needs no key) and Openverse for each shot. Filter licences to CC0, CC BY and CC BY-SA (never NC or ND), crop to 16:9, and add a slow camera push. curl -s "https://commons.wikimedia.org/w/api.php?action=query&list=search&srnamespace=6&srsearch=filetype:video%20job%20interview&format=json" Result. No usable free video exists for offices, receptions or interviews: the hits are under 720p or are news footage. So all five shots became photos. Each shot shows a different person, most aren't Indian, the "lounge" is a bus terminal, and in shot 1 we only see the back of a head. Time: ~22 min of searching and reviewing. GPU: none. Verdict: real, and legally clean with attribution, but it can't tell your story with your character. Pexels has much better free video, but needs an API key. Method 2: AI images with SDXL (the winner) How it works. Write one fixed character description and reuse it in every shot prompt. Fix the seeds, generate 2 options per shot, and pick the best. AARAV = "Aarav, a 26 year old Indian man, short neat black hair, light stubble, navy blazer over a white shirt" STYLE = "photorealistic, candid documentary photo, 35mm lens, natural light, shallow depth of field, ..." vae = AutoencoderKL.from_pretrained("madebyollin/sdxl-vae-fp16-fix", torch_dtype=torch.float16) pipe = StableDiffusionXLPipeline.from_pretrained("SG161222/RealVisXL_V4.0", vae=vae, torch_dtype=torch.float16, variant="fp16").to("mps") pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config, use_karras_sigmas=True) img = pipe(f"{AARAV}, sitting on a sofa ... looking down at his smartphone, {STYLE}", negative_prompt=NEG, width=1344, height=768, num_inference_steps=30, guidance_scale=5.5, generator=torch.Generator("cpu").manual_seed(1000)).images[0] The fp16-fix VAE matters: the stock SDXL VAE in fp16 on Apple GPUs tends to decode black or NaN images. !All 10 SDXL candidates, two per shot All 10 candidates, two per shot. Choosing took seconds. Result. These look like candid office photos, and Aarav is recognisably the same person in most shots. Weaknesses: - In the busy interview scene his identity drifted (glasses and a beard appeared). - Prompts are followed loosely: no receptionist actually scans his phone. - The images are stills, so motion comes only from the camera. Time: 108–131 s per 1344×768 image (average 119 s), so 16 images took ~28 min. GPU: yes (MPS, fp16). Verdict: ⭐ the best realism per minute. Method 3: AI video with LTX-Video (alive, but fragile) How it works. Animate the method 2 stills with LTX-Video 0.9.5 image-to-video, prompting only the motion. Two memory tricks are essential on a Mac: pipe = LTXImageToVideoPipeline.from_pretrained("Lightricks/LTX-Video-0.9.5", text_encoder=None, torch_dtype=torch.float32) # 1) encode every prompt once on the CPU, then drop the 19 GB text encoder before generating pipe.text_encoder = T5EncoderModel.from_pretrained("city96/t5-v1_1-xxl-encoder-bf16", torch_dtype=torch.float32) emb = {s: pipe.encode_prompt(MOTION[s], NEG, do_classifier_free_guidance=True, device="cpu") for s in shots} pipe.text_encoder = None; gc.collect() pipe.to("mps"); pipe.vae.enable_tiling() # 2) tiled decoding pe, pm, ne, nm = (t.to("mps") for t in emb["s1"]) frames = pipe(image=still, prompt_embeds=pe, prompt_attention_mask=pm, negative_prompt_embeds=ne, negative_prompt_attention_mask=nm, width=768, height=448, num_frames=121, num_inference_steps=30, guidance_scale=3.0, generator=torch.Generator("mps").manual_seed(42)).frames[0] Result. For one person with small motion it's genuinely convincing: Aarav smiles at his phone, and in the interview he talks and gestures while the panel listens. !Interview clip: natural talking and gesturing The interview clip at 0 s, 2.5 s and 5 s: natural talking and gesturing. Crowds and big motion break it. The busy reception melts into a blur after ~3 s, and walking out of the building warps his body. !Reception clip: fine at 0s, morphing at 2.5s, melted at 5s The reception clip at 0 s, 2.5 s and 5 s: the crowd melts. I rescued those two clips by keeping the clean first ~3 s and playing them forward then backward (ffmpeg ... trim,split,reverse,concat). The raw clips are in the repo. Time: - Quick test (49 frames, 20 steps): 278 s. - Full clips (121 frames ≈ 5 s, 30 steps): 811, 681, 1,568, 1,381 and 804 s, ~87 min for all five. GPU: yes (MPS, fp32; see the pitfalls). Verdict: use it for 2–3 hero moments, not a whole film. Method 4: Blender 3D with free rigged characters How it works. - Download CC0 rigged and animated characters (Quaternius, via Poly Pizza; no login). - Recolour them to match the cast and build sets from primitives. - Script every shot in Blender Python and render headless with EEVEE on the GPU: ~/Applications/Blender.app/Contents/MacOS/Blender -b -P shots.py -- s1 full 1920 1080 24 Result. Every shot does exactly what the storyboard asks. He holds the phone to the scanner, the phone buzzes, he walks out, and nobody is stuck in a T-pose. But it looks like a video game: - faces have no expressions and there's no lip-sync; - the same three base models are reused for everyone; - the sets are untextured. Realistic free 3D humans are hard to get: Mixamo needs an Adobe login, and MetaHuman and Character Creator aren't free. Time: ~35 min in total, of which 14 min was rendering 878 frames at 1080p. GPU: yes (Metal, EEVEE). Verdict: total control and consistency, but no realism. Method 5: a talking AI narrator with SadTalker How it works. 1. Generate a fictional presenter face with SDXL (method 2). 2. Lip-sync it to the full narration with SadTalker. 3. Clean up the face with GFPGAN. 4. Show it as a corner bubble. python inference.py --driven_audio narration.wav --source_image presenter.png \ --still --preprocess crop --enhancer gfpgan --size 256 Result. The lip-sync is frame-accurate across the full 30 s, but only the head moves, and at 512×512 it only works small. Match the face to the voice: my first presenter was a woman against a male voice, and it looked wrong. Time: - On the CPU: 24 s per step, projected at ~2.5 h. - On the GPU after a patch: 11.7 min, about 13× faster. - The GFPGAN clean-up then took 39 min on the CPU, because GFPGAN has no Apple GPU path. GPU: partly. Verdict: a nice guide or presenter; it can't carry the story. Results | | 1 · Stock | 2 · AI images | 3 · AI video | 4 · Blender | 5 · Narrator | |---|---|---|---|---|---| | Realism | 5 | 4 | 4 | 2 | 3 | | Same character throughout | 1 | 4 | 4 | 5 | 5 | | People actually move | 1 | 1 | 4 | 3 | 2 | | Matches the storyboard | 2 | 4 | 3 | 4 | n/a | | Speed | 4 | 4 | 1 | 3 | 2 | | Reliability | 4 | 5 | 2 | 4 | 3 | | Licence clarity | 3 | 4 | 3 | 5 | 4 | | Average | 2.9 | 3.7 | 3.0 | 3.7 | 3.2 | | Time for 30 s | ~22 min | ~28 min | ~87 min | ~35 min | ~52 min | | Ran on | CPU | GPU | GPU | GPU | GPU + CPU | Blender ties SDXL on the average only because consistency and licence clarity weigh as much as realism. If "real-looking people" is the goal, SDXL wins clearly. Where the time actually goes on a Mac | Step | Device | Measured | |---|---|---| | SDXL image, 1344×768, 30 steps | GPU | 108–131 s | | LTX-Video: generating a 5 s clip | GPU | 27–29 s (~1 step/s) | | LTX-Video: decoding + swap for that clip | GPU + swap | 650–1,540 s | | Blender, 878 frames at 1080p | GPU (Metal) | 14 min | | SadTalker face renderer, 380 steps | CPU → GPU | 24.1 → 1.85 s per step | | GFPGAN clean-up, 760 frames | CPU | ~3.1 s per frame | | Assembling one 30 s video | CPU + headless Chrome | 63–93 s | The biggest surprise: the AI video model isn't slow at generating. 95% of each clip's time went to decoding the frames while macOS swapped up to 32 GB to disk. The T5 encoder alone is 19 GB in fp32, and my IDE and VM were open. Unloading T5 after encoding cut the swap to 17.5 GB. Closing other apps is the single biggest speed-up. Pitfalls and fixes | Symptom | Cause | Fix | |---|---|---| | Input type (float) and bias type (c10::BFloat16) should be the same | M1 has no native bf16 | Load LTX-Video in fp32; it costs ~2× the memory | | Expected a 'mps' device type for generator but found 'cpu' | A CPU random generator fed to an MPS pipeline | torch.Generator("mps") | | SadTalker: Conv3D is not supported on MPS | Its pinned PyTorch 2.1 | Upgrade to PyTorch 2.5.1 and apply this 3-file patch | | SadTalker: invalid type: 'torch.mps.FloatTensor' | CUDA-style .type() calls | Covered by the same patch | | No module named 'pkg_resources' | New setuptools | uv pip install "setuptools<70" | | basicsr import error on functional_tensor | New torchvision | Change the import to torchvision.transforms.functional | | SDXL produces black or NaN images in fp16 | The stock VAE overflows | Use madebyollin/sdxl-vae-fp16-fix | | A 30 GB download takes hours | One slow stream | Download models in parallel with hf download, and use the bf16 T5 copy (9.5 GB, not 19 GB) | | brew install --cask blender asks for a password | The cask needs sudo | Copy Blender.app from the official DMG into ~/Applications | Which method should you use? - You need a recurring, realistic main character → SDXL stills with a fixed character description and seeds, plus camera motion. Generate 2–3 options per shot. - One hero moment needs real motion (a smile, a nod, someone talking) → LTX-Video on that SDXL still. One person, small motion, keep the clean seconds. - The shot has a crowd or a person walking → keep it a still. Today's local video models melt here. - You need exact staging (someone holds a phone to a scanner at a precise moment) → Blender, if a game look is acceptable. - You want a presenter face → a SadTalker bubble with a voice-matched face. - You need real people and don't care who → stock, ideally Pexels with a free API key. Limitations - One machine and one run per clip. No repeated runs, so no variance numbers. An M3 or M4, or a Mac with less load, will be faster. - The scores are my judgement, not a user study. The repo has every output so you can judge for yourself. - I picked the best of 2 seeds for the stills and trimmed two AI video clips. That's realistic practice, but it is curation. - On an NVIDIA GPU (CUDA, bf16) the AI video steps would be many times faster; this is a Mac-specific result. Reproduce it git clone https://github.com/kartikjindal/local-character-video-5-methods-benchmark && cd local-character-video-5-methods-benchmark uv venv .venv-ai --python 3.11 && uv pip install --python .venv-ai/bin/python -r requirements-ai.txt ./download-models.sh # ~23 GB, in parallel .venv-ai/bin/python gen_stills.py --seeds 2 # method 2 .venv-ai/bin/python gen_video.py --frames 121 --steps 30 # method 3 python3 -m http.server 8777 # dashboard: compare all five The SadTalker and Blender steps are in the README. Conclusion Free and local gets you surprisingly far. SDXL stills give realistic, consistent characters for ~2 minutes of GPU time per shot, and that should be the backbone. Local AI video is real but narrow: magic for one person making small movements, unusable for crowds, and slow on a Mac mainly because of memory, not compute. Everything here cost $0 and about 28 GB of disk. What would you try next? I'm curious whether a newer local video model, or a Mac with more memory headroom, changes the AI video verdict. If you've run LTX-Video, Wan or HunyuanVideo on Apple Silicon, share your timings in the comments. --- Resources - ▶ Live side-by-side dashboard (all five videos in sync) - 📦 Repo: inputs, outputs, scripts, logs - 📄 Full report with every measured number - Tools: RealVisXL V4 · LTX-Video · SadTalker · GFPGAN · Blender · Quaternius · Kokoro TTS · HyperFrames · ffmpeg All AI faces in this post are fictional.

  6. YouTube · · YouTube description · Sigma Box

    Rust Is Winning AI's Hidden Layer In September, GitHub said one engineer and a team of AI agents moved the Copilot agent runtime to Rust. Bun, the runtime under Claude Code, was rewritten in Rust with Claude agents. OpenAI's Codex CLI went Rust in 2025. But almost everyone in AI writes Python, so what's going on? In this video we follow one prompt down the stack and look at where the Rust actually is: the agent CLI, the tokenizer, the model weights, the server front door and your Python tools. Then we look at why Python servers and C++ both run into trouble, why AI agents suddenly made big rewrites cheap, and where Rust doesn't help (Cloudflare's outage, compile times, Microsoft's 2030 walk-back). Short version: Python stays where people think and try things, Rust takes the parts that run all day, and CUDA C++ still does the GPU maths. All numbers are the companies' own, as stated in their posts: GitHub, Migrating the GitHub Copilot runtime to Rust: github.blog... Bun, Rewriting Bun in Rust: bun.com OpenAI Codex CLI is going native: github.com/openai/codex/discu... tiktoken: openai/tiktoken Safetensors joins the PyTorch Foundation: huggingface.co... vLLM Rust front-end RFC: vllm-project/vllm... uv: astral-sh/uv What's new in Python 3.14: docs.python.org.... Chromium memory safety: chromium.org... Google, Rust in Android: blog.google... LWN, The state of the kernel Rust experiment: lwn.net Cloudflare outage, Nov 18 2025: blog.cloudflare.com... The Register on Microsoft's 2030 post: theregister.com... Astral to join OpenAI: astral.sh Why AI companies are moving to Rust for their core agent runtimes. Learn how this shift prevents memory bugs and improves runtime performance. Major AI organizations are transitioning their infrastructure from C++ and Python to Rust to ensure memory safety and stability. This video examines why companies like GitHub and OpenAI are rewriting their agent runtimes, focusing on the technical advantages of Rust for process startup and in-process execution. We follow one prompt down the stack to see where these languages actually sit in modern AI development. We also address the limitations of this transition, including compile times and the specific areas where Python and CUDA C++ remain essential. By looking at real-world deployments and recent industry reports, you will understand the trade-offs involved in adopting memory safe languages and why agent-based architectures have made these large-scale rewrites a viable strategy for infrastructure teams. Subscribe to SigmaBox for more engineering-focused AI research breakdowns. Tell us in the comments: do you think the industry will eventually move the majority of AI tooling to Rust?

    1 like · 24 views
  7. YouTube · · YouTube description · Vishal

    AI Agents Are Quietly Being Rewritten in Rust OpenAI, Block, and Perplexity all rebuilt key parts of their AI stack in Rust. In this video I break down why the layer around the model (agents, sandboxes, and tooling) is moving to Rust, and the one catch most people skip. I’m a fresh grad learning Rust and harness engineering in public, so every claim here comes from sources I checked, linked below. Chapters 0:00 Elon’s Larp 00:36 Codex CLI: TypeScript to Rust 01:46 Goose: Python to Rust 02:22 Sandboxes: Firecracker and Perplexity SPACE 03:14 uv, Ruff, and OpenAI buying Astral 03:50 Hugging Face tokenizers and Candle 04:13 The catch 04:45 Advance Sources Elon post : x.com openai/codex block/builderbot perplexityai/SPACE x.com codex#1174 news.ycombinator.com reddit.com codex#1174 news.ycombinator.com reddit.com github.com/aaif-goose/goose/blob/main/crates/goos… goose-docs.ai github.com/aaif-goose/goose/blob/main/crates/goos… firecracker-microvm.github.io x.com anthropics/claude-code reddit.com huggingface.co huggingface/candle astral-sh/uv astral.sh github.com/perplexityai/SPACE/tree/main/crates/ap… firecracker-microvm/firecracker github.com/aaif-goose/goose/tree/main Follow along Rust playlist: youtube.com Building Harness playlist: youtube.com Portfolio: vkgeed.in X post for this video: x.com #rust #aiengineering #softwareengineering

    2 likes · 1,199 views
  8. Building a $0 VPS on Google Cloud: DietPi, Tailscale, IPv6, and the traps I hit I wanted a small always-on Linux box: private SSH access, no public IPv4, and a bill of exactly zero. The job is modest — a dashboard for the box itself, a couple of small scripts, an SSH endpoint I can reach from anywhere — but renting a VPS for that is paying monthly for a machine that mostly idles. What I got was a working VM, after falling into five traps and several installer bugs that cost me an evening. The last step was not declaring victory: it was opening Cloud Billing to see what the platform actually charged. This is the field report, including the parts where I was wrong. The target - Google Cloud e2-micro (2 shared vCPU, 964 MB RAM) in a free-tier region - Debian 13 "Trixie" base, DietPi on top - No public IPv4 - SSH over Tailscale only - Budget-triggered billing shutoff as a safety net, not an absolute spending guarantee The RAM number matters. On plain Debian the box idled at 389 MB used. On DietPi it idles at 248 MB — 36% less, which on a 964 MB machine is the difference between "can run one small service" and "cannot". Trap 1: the DietPi conversion script will eat your Tailscale DietPi ships PREP_SYSTEM_FOR_DIETPI.sh, which converts a running Debian install into DietPi. It looks like the obvious path on a cloud VM: no image juggling, just run it. Don't — not on a cloud VM you only reach over the network. Reading the script before running it: grep -in "tailscale" prep.sh # no matches grep -Ein 'google|gcloud' prep.sh # no matches And the mechanism, at line 995: G_DIETPI-NOTIFY 2 'Marking all packages as auto-installed first, to allow effective autoremove afterwards' mapfile -t apackages < <(apt-mark showmanual) G_EXEC apt-mark auto "${apackages[@]}" It marks every manually-installed package as auto-installed, then autoremoves what nothing depends on. Unless explicitly protected, tailscaled and the GCP guest agents (google-guest-agent, google-compute-engine-oslogin) are candidates for removal when nothing else depends on them. The script's own header says it plainly: > # - Either Ethernet connection or local (non-SSH) terminal access On a remote-only VM, that cleanup can cut off both Tailscale and the guest-agent path you expected to use for recovery. I did not want to discover that halfway through a conversion. What I did instead: built a new VM from the official DietPi image, keeping the old one running until the new one answered. The old VM was the rollback plan, and it stayed untouched until the end. Trap 2: GCP only accepts a file literally named disk.raw Downloading DietPi_NativePC-UEFI-x86_64-Trixie.img.xz and repacking it naively fails: ERROR: (gcloud.compute.images.create) Could not fetch resource: - The file inside the tar archive was named 'dietpi.img'. It should be named 'disk.raw'. The tar must contain exactly one file, named disk.raw. Here is the sequence used after downloading the named DietPi image (replace BUCKET with your bucket name): mv DietPi_NativePC-UEFI-x86_64-Trixie.img.xz dietpi.img.xz xz -dk dietpi.img.xz cp dietpi.img disk.raw tar -cSf disk.tar disk.raw # -S for sparse gzip -1 disk.tar gcloud storage cp disk.tar.gz gs://BUCKET/ gcloud compute images create dietpi-trixie \ --source-uri=gs://BUCKET/disk.tar.gz \ --guest-os-features=UEFI_COMPATIBLE --guest-os-features=UEFI_COMPATIBLE is not optional — the DietPi Native PC image boots via UEFI, and the GCE firmware needs to know. Trap 3: the VM came up with a public IP and a default password I intended to create the instance without a public IPv4. My first verification showed an external address, an overly broad SSH firewall path, and Dropbear still accepting the vendor's default password. Whether the flag was omitted or the effective deployment settings differed, the lesson is to inspect the resulting instance instead of trusting intent or the command line. That exposure was brief, but brief is not harmless. Rotate defaults immediately, use SSH keys rather than password authentication where possible, and explicitly inspect both IPv4 and IPv6 firewall rules. Always verify immediately after creation: gcloud compute instances describe VM --zone=Z --format="value(networkInterfaces[0].accessConfigs[0].natIP)" Don't trust the flag; check the result. Trap 4: no egress, and Tailscale status misled me This one cost the most time, and the symptom is genuinely misleading. Removing the external IPv4 broke everything. tailscale status on the peer still showed the node as active, with tx/rx counters. But: $ tailscale ping 100.x.y.z ping "100.x.y.z" timed out active in the tailnet is not proof of reachability. The node's tailscaled was registered with the coordination server through a stale path and kept reporting itself; data packets had nowhere to go. When you are debugging, an actual end-to-end probe is better evidence of reachability than a status label. Two separate causes stacked here: 4a. The VM was on the wrong VPC I created it on default, which is IPv4-only. The working VM was on a custom VPC with stackType: IPV4_IPV6. Same symptom, different root cause. Compare against a VM that already works before creating a new one — one describe would have saved an hour. 4b. GCE expects DHCPv6; my DietPi network configuration did not complete it With the right VPC and an external IPv6 range assigned, the interface still had only a link-local address: $ ip -6 addr show eth0 inet6 fe80::4001:aff:fe14:3/64 scope link proto kernel_ll Google assigns a /96 IPv6 range to an interface, with the first address delivered using DHCPv6. The metadata server also provides router advertisements for the default route. My DietPi image, using ifupdown, did not complete that configuration automatically, while the earlier Debian install had handled it. This is an issue with the guest configuration, not proof that GCE lacks DHCPv6 or router advertisements. See the official IPv6 setup documentation. The workaround I used, in /etc/network/interfaces.d/eth0.conf, with the address and gateway obtained from the actual instance configuration (the values below are placeholders): allow-hotplug eth0 iface eth0 inet dhcp iface eth0 inet6 static address 2600:1900:4000:XXXX:0:0:0:0 netmask 96 gateway fe80::4001:aff:fe14:1 Validate before rebooting — ifupdown can still boot you into a networkless machine. Have a recovery path before editing remote network configuration: ifquery eth0 # exit 0 and the parsed values mean the syntax is good Then reboot for real. A systemctl restart networking is not the same test; the bug only shows on a cold boot. Trap 5: IPv6-only egress cannot reach IPv4-only destinations Getting IPv6 working is only half the story. The VM had no external IPv4 and no usable native IPv4 internet egress in this setup. (It still had an internal IPv4 address on its dual-stack VPC interface.) IPv4-only destinations remained out of reach. The symptom looks exactly like a firewall problem: $ curl -sSfL https://github.com/astral-sh/uv/releases/download/0.13.4/uv-x86_64-unknown-linux-gnu.tar.gz curl: (28) Failed to connect to github.com port 443 after 134331 ms: Could not connect to server 134 seconds of silence, then nothing. No connection refused and no DNS error: the hostname resolved, but our VM had no available native route to the IPv4 endpoint returned for that download. The instinct is to go hunting through ip addr, ip route, and firewall rules. The explanation for this incident was one DNS query away: at the time of the test, github.com returned no usable AAAA record for the VM. DNS records and endpoint support change, so repeat the check rather than assuming the same answer forever. $ getent ahostsv6 github.com # empty I did not trust the local resolver, so I asked a public one too: curl -sS -H 'accept: application/dns-json' \ "https://cloudflare-dns.com/dns-query?name=github.com&type=AAAA" # {"Status":0,"Answer":null} -> no AAAA These were the hostname results captured during the incident, not a permanent current-status table: | Host | AAAA | |---|---| | github.com | no | | api.github.com | no | | objects.githubusercontent.com | no | | release-assets.githubusercontent.com | no | | raw.githubusercontent.com | yes | | github-releases.githubusercontent.com | yes | | pypi.org, files.pythonhosted.org | yes | | deb.debian.org, astral.sh | yes | In these tests, PyPI and the Debian mirrors worked over native IPv6 while the tested GitHub release URL did not. That is a snapshot of this environment, not an eternal rule for every GitHub hostname. A network configuration cannot conjure an IPv6 endpoint where one is unavailable. The fix: a proxy on a dual-stack peer, not an exit node Two options, and I tried the reasoning for both. Exit node. Route everything through a Tailscale peer that has IPv4. It works — and I rejected it. It sends 100% of the traffic through a home connection, including the PyPI and Debian traffic that is already fine over native IPv6. It adds latency to everything, and it turns that peer into a single point of failure for the entire VM: one machine reboots and the server loses the internet. A proxy for only what needs it. What I built: a small HTTP CONNECT proxy on a dual-stack machine in the same tailnet, bound only to the Tailscale interface. Traffic splits in two: ┌──────────────────────┐ pypi.org ───► │ │ native IPv6 — direct deb.debian.org │ marine (IPv6-only) │ │ │ github.com ───► │ apt / curl ────────┼──► dual-stack peer ──► IPv4 internet (IPv4-only) └──────────────────────┘ 100.x.y.z:3128 The proxy itself is a single constant — the rest is deployment: BIND = ("100.x.y.z", 3128) # Tailscale IP, never 0.0.0.0 with a systemd unit so it comes back after a reboot: [Unit] Description=CONNECT proxy for the IPv6-only VM After=network-online.target tailscaled.service Wants=network-online.target [Service] ExecStart=/usr/bin/python3 /opt/marine-proxy/proxy.py Restart=always RestartSec=5 [Install] WantedBy=multi-user.target Binding to the Tailscale address rather than 0.0.0.0 is not a detail. An open proxy on a LAN is a gift to whoever scans it first; the tailnet is already the trust boundary. On the VM side, the proxy has to be visible to processes that never read a shell profile — apt, systemd units, installer scripts: # /etc/apt/apt.conf.d/99proxy Acquire::http::Proxy "http://100.x.y.z:3128"; Acquire::https::Proxy "http://100.x.y.z:3128"; And because DietPi's installer shells out to curl from a non-interactive process that inherits neither /etc/profile.d nor /etc/environment: # /root/.curlrc proxy = "http://100.x.y.z:3128" noproxy = "localhost,127.0.0.1,::1,100.64.0.0/10,.ts.net,.debian.org,pypi.org,.pythonhosted.org" /root/.curlrc is read by curl itself when it runs as root, even if the calling shell did not load interactive profiles. The noproxy half is the important half for curl — without it, even IPv6-ready traffic goes to the proxy. Note that this .curlrc does not set bypass rules for APT: the APT proxy configuration above applies separately, and per-host APT exceptions require their own settings. 100.64.0.0/10 in noproxy is not optional — it is the Tailscale CGNAT range. Omit it and the proxy tries to reach the tailnet through itself. The result, measured on the VM: | Destination | Route | Result | |---|---|---| | pypi.org | direct, native IPv6 | 141 MB/s | | github.com | through proxy | 200 | | GitHub release tarball | through proxy | 16 MB in 18s (was a 134s timeout) | The final proxy setup at a glance | Piece | Where | What it does | |---|---|---| | proxy.py + systemd unit | dual-stack peer | HTTP CONNECT on 100.x.y.z:3128, bound to the tailnet address only | | /etc/apt/apt.conf.d/99proxy | marine | APT sends http/https through the proxy | | /root/.curlrc | marine | curl as root uses the proxy, with noproxy for the tailnet and IPv6-ready hosts | | /etc/apt/apt.conf.d/99force-ipv6 | marine | deleted — forcing a family breaks the proxy connection | | /etc/gai.conf (optional) | marine | prefer IPv6 through precedence, without forbidding IPv4 | That last row is the whole next section in one line. The trap inside the fix: prefer IPv6, never force it Earlier, when apt kept stalling on IPv4, I had "fixed" it with: # /etc/apt/apt.conf.d/99force-ipv6 Acquire::ForceIPv6 "true"; That was correct at the time and became wrong the moment the proxy existed: Err:1 https://deb.debian.org/debian trixie InRelease Something wicked happened resolving '100.x.y.z:3128' (-9 - Address family for hostname not supported) ForceIPv6 applies to the proxy connection as well, and the proxy is an IPv4 literal. Forcing a family is a landmine; preferring one is fine. For processes using glibc address selection, /etc/gai.conf can adjust precedence without globally forbidding IPv4. Test it against the applications you actually use: precedence ::ffff:0:0/96 10 Delete the ForceIPv6 file. Prefer, never force. The counterintuitive part: IPv6 is free, IPv4 is not I initially assumed the external IPv6 was costing money and estimated ~$3.65/month. That was wrong, and the documentation says so in a sentence I read past: > "You are not charged for external IPv6 address ranges that are assigned to subnets or for external IPv6 addresses that are assigned to VM instances." The $0.005/hour line in the same table is for IPv4: > "Static and ephemeral IP addresses in use on standard VM instances — $0.005 / 1 hour" > Free tier: limited to one hour per month. So on a free-tier VM: | Address configuration | Address charge | Caveat | |---|---|---| | External IPv6 on a VM | $0 for the address | Data transfer has separate pricing and limits | | External IPv4 on a VM | Charged hourly in normal use | Both static and ephemeral IPv4 can be charged | | No external IP address | No external-address charge | Internet egress needs another path | In this design, external IPv6 gave the VM outbound connectivity without paying for a public IPv4 address. It did not make all outbound traffic unlimited or free. See Google Cloud VPC pricing. Cloud NAT is another way to provide IPv4 egress without a public IPv4 on the VM, but it has its own charges; verify the pricing before treating it as a Free Tier component. What the image actually carries — and what I installed myself The VM was provisioned from the DietPi Native PC image, built for bare metal. It does ship support for hardware a cloud VM will never have — but less than my first pass at this audit claimed, and the correction is the more useful lesson. I went back to the package manifest of the image as imported, before anything was added to it. What it actually carries: | Shipped by the image | Weight | Present on the VM | |---|---|---| | firmware-iwlwifi (Intel) | 111 MB | no wireless adapter | | firmware-atheros | 95 MB | no wireless adapter | | firmware-mediatek | 45 MB | no wireless adapter | | firmware-realtek, firmware-brcm80211 and two smaller ones | 38 MB | no wireless adapter | | Wi-Fi firmware, total | 288 MB | 41% of the 698 MB base system | That is the vendor's real waste: 288 MB of firmware for three wireless vendors on a machine with no radio. It is removable with no loss of function, and it is the part of this cleanup I would apply to any DietPi cloud VM on sight. Everything else that looked like image bloat was mine. Docker, the LEMP stack, Mesa/libllvm19, intel-microcode and ffmpeg were not in the imported image — they were installed on this box afterwards and then forgotten. An earlier version of this audit listed them as things "the image carries", and that was wrong. It changes the conclusion: the expensive part was not the vendor's defaults, it was what I had installed myself and never used. So the cleanup had two different targets, and only one of them is a default-image problem: - Vendor defaults — 288 MB of Wi-Fi firmware for hardware that is not there. - My own installs — Docker with zero containers, a LEMP stack with no application, a graphics stack with no GPU. Reversible, but self-inflicted. The starting point, counting both: 3.8 GB used of 9.8 GB (40%) and 13 running systemd units. After removing what would never run — measured at the end of the cleanup run: | | Before | After | |---|---:|---:| | Disk used | 3.8 GB (40%) | 2.7 GB (28%) | | Running systemd units | 13 | 7 | With ~721 MB of MemAvailable on a 964 MB machine, up from ~656 MB. I am deliberately not quoting a package count here. The cleanup report I started from claimed 526 → 452, and when I went back to check it against the dpkg manifests, no snapshot on the box ever held 526 packages: the imported image had 223, the snapshot from the night before had 249. The disk and memory numbers are real — they come from df and free captured inside the run. The package count was not, and I would rather drop it than publish a number I cannot reproduce. Those numbers describe the box at 12:21 that day. They are a snapshot, not a permanent state — part of the LEMP stack was reinstalled afterwards, on purpose, and the count moved again. Two traps in that cleanup are worth more than the numbers themselves. The dependency you cannot see This one did not bite me on this box — I never had ffmpeg installed here, and I checked. But it is the check I now run before any purge, because the chain is real: ffmpeg -> libavfilter10 -> libflite1 libflite1 is a speech-synthesis engine. On a headless server it looks like an obvious orphan. ffmpeg uses the flite filter to generate synthesized audio, so it depends on libavfilter, which depends on libflite1. Purging the speech library takes the video tool with it. The way to see it before you commit is the simulation, and it only works if you read the lines you did not ask for: apt-get -s purge libflite1 | grep '^Purg' # Purg ffmpeg libavdevice61 libavfilter10 libflite1 Three names appear that were never in the list you typed. A dependency tree is not the list of names you chose; it is the list of names you are about to lose. The summary line tells you how many; only the Purg lines tell you which. autoremove goes further than the plan After the explicit purge, apt-get autoremove --purge removed packages that were not on my list — among them rsync and nftables. Both had been installed as dependencies and became orphans. I only noticed rsync was gone when a later install pulled it back in, hours afterwards; nftables I never reinstalled. The impact is low — everything here reinstalls in one command — but the lesson is structural. autoremove is where a cleanup plan loses control. apt tells you how many packages it will remove, never which. Either read the full list, or mark what must survive before running it: apt-mark manual rsync lsof socat nftables Building a budget-triggered billing kill switch Here is the part that surprises people: a standard GCP budget alert does not enforce a spending cap. It can email or publish notifications, but usage continues unless a separate action is taken. Google documents a possible automation to disable billing; it also warns of reporting delays. To add an automated shutoff attempt, I chained three pieces: 1. A budget with a pubsubTopic in notificationsRule 2. A Cloud Function subscribed to that topic 3. A service account allowed to call projects.updateBillingInfo with an empty billingAccountName Cloud Billing │ ▼ Budget: R$5 · alerts at 50 / 90 / 100% │ notificationsRule.pubsubTopic ▼ Pub/Sub topic ──► Eventarc ──► Cloud Function (gen2) │ │ costAmount / budgetAmount >= 1.0 ? ▼ projects.updateBillingInfo billingAccountName = "" Unlinking the billing account can stop billable services, but the trigger is not instantaneous. Charges incurred before notifications arrive may still post afterward. This is a safety mechanism, not a contractual hard spending cap. See Google’s own limitations. The core of the function is one API call: def disable_billing(project_id): client = _billing_client() name = f"projects/{project_id}" current = client.projects().getBillingInfo(name=name).execute() if not current.get("billingEnabled"): return "already_disabled" client.projects().updateBillingInfo( name=name, body={"billingAccountName": ""} ).execute() return "disabled" Four things that bit me building it The budget currency must match the billing account currency. Creating a USD budget on a BRL account returns INVALID_ARGUMENT with no useful detail. Read currencyCode from the billing account first. pubsubTopic: "" is also INVALID_ARGUMENT. Omit the field entirely if you want email-only notifications. ADC needs a quota project for the budgets API. A curl with a valid bearer token returns: 403: "Your application is authenticating by using local Application Default Credentials. The billingbudgets.googleapis.com API requires a quota project" Add the header x-goog-user-project: your-project. Cloud Run returns 200 even when your function crashes. The gen2 runtime wraps your entry point in the background signature, so it calls function(event, context) — two arguments. I wrote def handler(cloud_event), and the only evidence was buried in stdout: TypeError: budget_guard() takes 1 positional argument but 2 were given while the high-level HTTP log line I had inspected said 200. Do not use an HTTP status alone to prove that the intended handler ran successfully. Read the function/container logs: gcloud logging read 'resource.labels.service_name=MY_FN AND logName:"stdout"' --limit=20 Also: logging.info() did not appear in stdout for me. print(..., flush=True) did. And one IAM detail: the Eventarc trigger invokes Cloud Run as a service account, and Cloud Run rejects it unless that account holds roles/run.invoker on the service. The trigger exists and looks healthy either way. Testing without detonating I tested only the safe branch — publish a message with a cost below the threshold and confirm the function logs its decision and stops: [budget-guard] spend 1.50 of 27.00 (5.6%) — target project: my-project [budget-guard] below the 100% threshold. No action taken. The numbers in that test payload are arbitrary — I chose them to produce a ratio below 1.0, not to match the live budget. What the test proves is the branch, not the amounts. Testing the real path would have interrupted the VM. The non-destructive test confirmed the safe branch; code review gave us partial confidence in the destructive branch, which remained untested end-to-end. A stronger approach is to test the destructive branch against a disposable project, not production. Two design choices worth copying: - Hardcode the target project in an env var. Never infer it from the Pub/Sub payload — a malformed message should not be able to shut down the wrong project. - Abort if budgetAmount is missing or zero. A division by zero or a bad payload must fail closed, not open. Proving the $0 bill: the Marine billing audit This was the part that mattered most to me. I did not want to publish "free VPS" merely because the Compute Engine Free Tier page said e2-micro was eligible. A real project can still accumulate charges for surrounding services. We audited a Google Cloud project (id withheld), covering October 1–24, 2026, using its Cloud Billing reports. A read-only review compared the service totals, individual SKUs, savings, and available promotional balances. The audit's observed outcome was R$0.00 in net usage cost for the project during that period. > On the figures: the amounts below are the values Cloud Billing actually reported for the audited window, rounded to centavos the way the console displays them. They are small — cents, not dollars — and that is the point: the gross usage is real, the net is zero. | Reported SKU or service | Gross usage | Savings / offsets | Net usage cost | |---|---:|---:|---:| | Network Analyzer Resource Hours | R$0.12 | −R$0.12 | R$0.00 | | Internet to Google Cloud Performance Resource Hours | R$0.09 | −R$0.09 | R$0.00 | | Topology and Google Cloud Performance Resource Hours | R$0.12 | −R$0.12 | R$0.00 | | Cloud Run Functions CPU (us-central1; 124.72 vCPU-seconds) | R$0.02 | −R$0.02 | R$0.00 | | Network Intelligence rounding / minor residual | ≈R$0.01 | ≈−R$0.01 | R$0.00 | | Reported total | ≈R$0.36 | ≈−R$0.36 | R$0.00 | The displayed SKU rows are rounded to centavos, hence the small difference between their sum and the reported service total. Why gross usage showed up at all Google explicitly documents a 100% discount for Network Analyzer, Network Topology, and Performance Dashboard. Their list-price usage can appear in billing details even when the final charge is zero. That is precisely why hiding or deleting network monitoring would have been the wrong first response. Official Network Intelligence Center pricing. Cloud Run's Free Tier can also appear as gross compute usage followed by credits or savings. The 124.72 vCPU-seconds observed for this budget-related function were in us-central1 and showed R$0.00 net in the reviewed report. Official Cloud Run pricing. What the audit confirmed — and what it did not The comparison found no net charge on these lines, and no identifiable material drawdown of the Developer Program or other promotional balances attributable to them. The developer-credit balance was essentially unchanged; a roughly R$0.01 movement elsewhere in the balance display was too small and insufficiently attributed to use as proof either way. There was one remaining limitation: the console grouped these abatements under "other savings" without conclusively exposing credit.type and credit.name for every current-period SKU. We could verify which resource accrued gross usage, and that the usage was fully offset, but not exactly which credit record paid for each centavo in the underlying billing export. So the defensible claim is not "Google showed zero gross usage" or "we traced every cent of credit attribution." It is: the project's observed net usage cost was R$0.00 for the audited period; the relevant Network Intelligence Center services have a documented 100% discount; and the reviewed balances did not show a meaningful promotional-credit debit tied to those lines. A final attribution audit would require SKU-level credit.type / credit.name details, for example through detailed Cloud Billing export if enabled. For anyone reproducing this, visit Cloud Billing → Reports, filter by project and date, group by SKU, and compare Usage cost, Other savings, and Subtotal. Then repeat the view with Promotional credits excluded. Google documents these filters in View and analyze savings. What "$0" does not promise Google Cloud's always-free Compute Engine allowance is not unlimited. At the time of this audit it covered an e2-micro within the eligible US regions, 30 GB-month of standard persistent disk, and 1 GB/month of qualifying North American outbound data transfer, aggregated under the program rules. External IPv6 addresses assigned to VMs do not carry an address charge, but outbound traffic, additional disks, custom images, snapshots, and adjacent services can have their own pricing. See Google Cloud Free Program, VPC pricing, and custom image storage pricing. The audit found no separately identified nonzero net charge for the DietPi image, persistent storage, external IPv6, or data transfer in the reviewed window. That is a measurement of this period, not a promise about every future hour or every deployment. Verify your region, disk type, account-wide allowances, and future use; the project can become billable if conditions change. In short: the Marine was a zero-net-cost VPS in the period we actually checked. The article is a blueprint for staying inside the free allowances — not an unlimited-use guarantee or a vendor billing warranty. Final state DietPi 10.8 / Debian 13 (trixie) e2-micro, 964 MB RAM, 1.1 GB swap, 9.8 GB disk Disk: 40% -> 28% Units: 13 -> 7 (at the end of the cleanup run) External IPv4: none External IPv6: yes (free) SSH: Dropbear, restricted to the tailnet in tested setup; migrate to key-only auth Firewall: verify effective IPv4 **and** IPv6 ingress policies; management via tailnet IPv4-only destinations: CONNECT proxy over the tailnet, 100.64.0.0/10 excluded IPv6-capable destinations: native, direct Observed net GCP usage cost (1–24 Oct 2026): R$ 0.00 after savings Plus a R$5 budget with alerts at 50/90/100% and a function intended to unlink billing if the threshold is reached. The controller is useful defense in depth; it does not guarantee zero future bills. What I'd do differently 1. Read the conversion script before running it — four greps would have saved the whole rebuild. 2. describe a working VM before creating a new one. Network stack, image flags, disk type. Copy the working configuration instead of reconstructing it. 3. Treat tailscale status as a hint and tailscale ping as evidence. 4. Verify the external IP after creation, don't trust --no-address. 5. Check the pricing page's exceptions, not just its table. "You are not charged for..." sentences are easy to skip and change the answer completely. 6. Check the destination's AAAA before touching the network config. In this case, the 134-second timeout was about an IPv4-only destination, not a missing firewall rule. 7. Prefer a family, do not force it globally. ForceIPv6 broke APT once the proxy was configured with an IPv4 literal. 8. Test an installer's validation regex against the example the installer displays. If the example fails, the bug is not in your typing. 9. Read the Purg lines of a simulation, not its summary. The summary says how many packages will go; only the list says which ones — and a dependency tree can hide a tool you meant to keep behind a library you never think about. 10. Review removals and mark essential packages as manual before autoremove. Orphaned dependencies are not necessarily junk. The traps were not exotic. They were hiding in a script header, cloud networking docs, a pricing footnote, function logs, a DNS answer, and finally a billing report. The lesson is not "GCP is hard"; it is that the evidence was often one command or one billing filter away, and I trusted the convenient summary instead of verifying the underlying data. The one I keep thinking about is still the DNS check. I spent time inspecting routes and firewall rules for a destination that was unavailable over IPv6 in that test. getent ahostsv6 github.com was the useful next command. It took one second to run and an hour to think of. The billing report delivered the same lesson in a different form: a cost line is not the same as a charge, and a zero net bill is not proof that every underlying credit was individually identified. Appendix: four dietpi-dashboard installer bugs dietpi-dashboard was the first service I put on the VM, and its setup dialog fought IPv6 the whole way. Four bugs, all in the installer: 1. The address validator cannot accept IPv6. The regex in dietpi-software:3778 ends in an alphanumeric class, so it takes a hostname or an IPv4 address — never a colon. The dialog's own example (fe80::4001:aff:fe14:1:5253) is rejected by the validator displaying it. 2. The two validators disagree. The regex accepts hostnames, but the binary is Rust and parses the field as SocketAddr, so a hostname passes the dialog and dies at startup with invalid socket address syntax for key frontend_addr. What works is a bracketed IPv6 literal — rejected by the regex, so it has to be set by editing config-backend.toml. 3. The 64-character secret is never checked against the frontend's. With mismatched secrets the network is correct, the port is open, the service reports active, and the handshake fails silently. 4. The installer overwrites secret with whatever was typed, with no validation at all. Mine ended up as an 8-character string and broke the config after a reboot. One diagnostic trap belongs with them: the backend runs with no listening port on 5252 or 5253, which looks like a broken config and is not. In a backend-client architecture the node dials out and the frontend listens, so a node with no open port is correct behaviour. Confirm it from the frontend host instead: ss -tnp | grep 5253 # ESTAB [fd7a:...:9c5d]:5253 [fd7a:...:b278]:45650 users:(("frontend",...)) The practical rule: after the installer finishes, verify the config file instead of trusting the dialog, and diff the secret between hosts before starting the backend.

    1 reaction · 2 comments
  9. 分享个Python工具:uv。 它把包管理、虚拟环境和项目管理放到了一起。对我来说,这类工具最吸引人的地方是少记几套命令。 你们配Python环境最烦哪一步? astral-sh/uv #蓝V #蓝V互关 #开源项目

    3 likes · 183 views
  10. [quote="Henry Schreiner, post:4, topic:109629, username:henryiii"] m not very fond of arbitrary caps, though setting a limit of uint64 (or even uint32) on each element would be have a practical benefit. [/quote] FYI, here are real issues where people may exceeded uint64 or hit arbitrary limits: - Branch and commit hash in dev segment: uv#8271 - Date and commit hash in the dev segment: uv#9572 - Timestamps in the local segment: renovate#34101 - Commit and date labels in the local segment: uv#14137 I don't personally think it needs an update, but it tool/infrastructure/library authors think it needs a clarification, I think it's fine to add that tools may reject versions based on their size of individual components or the entire version. And that can be either character limits or byte limits, whatever makes sense for the tool, I think what the limit is is different for different contexts and I'm not sure it makes sense to be explicit in the spec.

    1 comment

Latest stargazers

The last 10 distinct accounts we observed this year.

Most active participants

PR, issue, comment & review events in the past year.

Packages

30
PackageVersionDownloads · 30dTrend · 30d
uv-auditv0.0.97
v0.0.97——
v0.0.97——
uv-authv0.0.97
v0.0.97——
———
v0.0.97——
uv-buildv0.13.5
v0.13.5——
v0.0.97——
v0.0.97——
v0.0.97——
uv-cachev0.0.97
v0.0.97——
v0.0.97——
v0.0.97——
uv-cliv0.0.97
v0.0.97——
uv-clientv0.0.97
v0.0.97——
v0.0.97——
v0.0.97——
uv-consolev0.0.97
v0.0.97——
———
uv-dirsv0.0.97
v0.0.97——
v0.0.97——
v0.0.97——
v0.0.97——
v0.0.97——
v0.0.97——
uv-errorsv0.0.97
v0.0.97——
uv-extractv0.0.97
v0.0.97——
uv-fastidv0.0.97
v0.0.97——
uv-flagsv0.0.97
v0.0.97——
uvv0.13.6
v0.13.6
≥140.5M
uv-buildv0.13.6
v0.13.6
≥15.2M

README badges

Embed a badge in your README or website.

  • astral-sh/uv reached #1 in daily stars on gitwal · Feb 16, 2024
  • astral-sh/uv reached #2 in weekly stars on gitwal · Week of Feb 12, 2024
Live mentions · last 30 days
astral-sh/uv mentions in the last 30 days