Breaking
Advertisement

AI

AI

NVIDIA Nsight Python Tutorial: How to Profile GPU Kernels

In this tutorial, we implement on Nsight Python, NVIDIA’s open-source Python interface to its kernel profilers. Instead of running a script under the Nsight Compute command line and digging through a report afterward, we mark the code we care about with nsight.annotate, decorate a benchmark function with nsight.analyze.kernel, and get back a pandas DataFrame of hardware metrics per annotated region, per configuration, and per run. Since version 1.0, the library attaches Nsight Compute to the running Python process, making it usable in Jupyter and Colab, and the upcoming 1.1 release adds an experimental CUPTI backend, explicit tool management, and custom information collectors. We run everything on a free Colab T4: we install a current Nsight Compute next to the one Colab ships, profile a first kernel, decide how many runs a measurement needs, measure what cache and clock control do to the same kernel, sweep matrix sizes in fp32 and fp16 with a derived TFLOP/s metric, use hardware counters to tell compute-bound from memory-bound work, measure kernel fusion in bytes moved, record clocks and temperature per run, compare the CUPTI backend with Nsight Compute, and keep the raw report for deeper analysis. Copy CodeCopiedUse a different Browserimport os import sys import glob import json import time import shutil import textwrap import traceback import subprocess RESULTS = {} def banner(title): print("n" + "=" * 78) print(title) print("=" * 78) def section(name): def wrap(fn): def run(*a, **kw): banner(name) try: out = fn(*a, **kw) RESULTS[name] = out if isinstance(out, str) else "ok" return out except Exception as e: RESULTS[name] = f"SKIPPED / FAILED -> {type(e).__name__}: {str(e).splitlines()[0][:160]}" print(f"n[!] {name} did not complete: {type(e).__name__}: {e}") traceback.print_exc(limit=3) return None return run return wrap def sh(cmd): """Run a shell command and return its stdout, stripped.""" return subprocess.run(cmd, shell=True, capture_output=True, text=True).stdout.strip() banner("1. Install a current Nsight Compute next to Colab's, then import nsight before CUDA") print(f" GPU: {sh('nvidia-smi --query-gpu=name,driver_version,memory.total,compute_cap --format=csv,noheader')}") preinstalled = shutil.which("ncu") print(f" ncu already on PATH: {preinstalled} -> {sh('ncu --version | tail -1') if preinstalled else 'none'}") # nsight-python needs Nsight Compute 2026.2.1 or newer. Colab ships an older ncu, but its image already # carries NVIDIA's CUDA apt repository, so a current release is one apt-get away. It installs into # /opt/nvidia/nsight-compute// and leaves the preinstalled one untouched. subprocess.run("apt-get update -qq > /tmp/ncu_install.log 2>&1", shell=True) for NCU_VERSION in ("2026.3.1", "2026.2.1"): if subprocess.run(f"apt-get install -y -qq nsight-compute-{NCU_VERSION} >> /tmp/ncu_install.log 2>&1", shell=True).returncode == 0: break else: print(sh("tail -5 /tmp/ncu_install.log")) raise RuntimeError("could not install Nsight Compute 2026.2.1+ from NVIDIA's apt repository") # ask dpkg where the package put the ncu launcher rather than assuming /opt/nvidia/nsight-compute// ncu_paths = sh(f"dpkg -L nsight-compute-{NCU_VERSION} | grep -E '/ncu$'").split() if not ncu_paths: raise RuntimeError(f"dpkg lists no ncu executable in nsight-compute-{NCU_VERSION}") NCU_DIR = os.path.dirname(min(ncu_paths, key=len)) # the top-level launcher, not a per-platform copy os.environ["PATH"] = NCU_DIR + os.pathsep + os.environ["PATH"] # nsight-python looks ncu up on PATH print(f" installed: {shutil.which('ncu')} -> {sh('ncu --version | tail -1')}") # nsight-python 1.1.0 (CUPTI backend, tool APIs, info collectors) is tagged but not yet on PyPI; # pin the commit this notebook was written against. The [cupti] extra is used in step 9. NSIGHT_COMMIT = "bb96db4260b1823d86ecab9acfed0646d7af79ad" subprocess.run([sys.executable, "-m", "pip", "install", "-q", f"nsight-python @ git+"], check=True) # The CUPTI backend needs cupti-python 13.4, which wants a newer libcupti than Colab's torch is pinned to. # Install it into a separate directory that only step 9's child process puts on its path. CUPTI_DIR = "/content/cupti_env" CUPTI_EXTRA = subprocess.run([sys.executable, "-m", "pip", "install", "-q", "--target", CUPTI_DIR, "--no-deps", "cupti-python>=13.4.0", "nvidia-cuda-cupti~=13.4", "pyelftools"]).returncode == 0 import nsight # activates Nsight Compute's injection library: must come before anything initializes CUDA from nsight.collection import ncu as ncu_backend import numpy as np import pandas as pd import torch SILENT = nsight.VerbosityLevel.SILENT pd.set_option("display.width", 200) pd.set_option("display.max_columns", 30) print(f" nsight-python {nsight.__version__} | active tool: {nsight.get_active_tool()}" f" | injection error at import: {ncu_backend.injection_load_error}") print(f" torch {torch.__version__} (CUDA {torch.version.cuda}) | {torch.cuda.get_device_name(0)}" f" | cupti-python installed: {CUPTI_EXTRA}") print("n Order matters: `import nsight` loads Nsight Compute into this Python process, and it can only") print(" attach to CUDA work started after that. Every profiled function below runs in this same") print(" process; nsight-python starts `ncu --mode attach` on its own PID for each one.") We start by checking the GPU and the profiler. Nsight Python requires the Nsight Compute command-line tool, ncu, at version 2026.2.1 or later, and finds it by looking it up on the PATH. Colab’s GPU image ships an older Nsight Compute. Still, it already has NVIDIA’s CUDA package repository configured, so we install Nsight Compute 2026.3.1 with apt-get, ask dpkg where the ncu launcher landed, and put that directory first on the PATH. We then install Nsight Python, pinned to the commit of the 1.1.0 pre-release this notebook was written against, and the cupti-python package into a separate directory, because it needs a newer CUPTI library than the one Colab’s PyTorch is pinned to. The import order matters: import nsight loads Nsight Compute’s injection library into the Python process, and it can only attach to CUDA work started after that, so nsight must be imported before torch touches the GPU. Copy CodeCopiedUse a different BrowserADD = lambda x, y: x + y # sums a metric if cuBLAS splits one matmul into several kernels @nsight.analyze.kernel(thermal_mode="off", combine_kernel_metrics=ADD) def first_matmul(n: int) -> None: a = torch.randn(n, n, device="cuda") b = torch.randn(n, n, device="cuda") with nsight.annotate("matmul"): _ = a @ b @section("2. Profile one kernel: what a ProfileResults row contains") def first_profile(): df = first_matmul(2048).to_dataframe() print(f"n to_dataframe() columns: {list(df.columns)}n") r = df.iloc[0] t = float(r["AvgValue"]) print(f" annotation {r['Annotation']!r}, n = {r['n']}, kernel {r['Kernel']}") print(f" {r['Metric']} = {t:,.0f} {r['Unit']} (runs {r['NumRuns']}, stable: {r['StableMeasurement']})") print(f" GPU {r['GPU']}, ComputeClock {r['ComputeClock']}, MemoryClock {r['MemoryClock']}") print(f" -> {2 * 2048 ** 3 / (t * 1e-9) / 1e12:.2f} TFLOP/s for a 2048 x 2048 fp32 matmul") print("n Only the kernel launched inside nsight.annotate() is profiled; torch.randn above it is not.") print(" With a single run, StableMeasurement is : one sample has no spread to judge.") return f"fp32 2048 matmul {t / 1e6:.3f} ms ({2 * 2048 ** 3 / (t * 1e-9) / 1e12:.2f} TFLOP/s)" first_profile() The first profile is a single fp32 matrix multiplication. The benchmark function creates its inputs, then wraps only the multiplication in nsight.annotate, so the random-number kernels that build the inputs are not measured. Calling the decorated function starts ncu in attach mode on the notebook’s own process, runs the function, and returns a ProfileResults object whose to_dataframe method gives one row per annotation, configuration, and metric. We print the columns and read the row: the kernel name, the measured gpu__time_duration.sum in nanoseconds with its unit, the number of runs, the GPU name and the clock attributes, and we turn the time into TFLOP/s by hand. The combine_kernel_metrics argument sums the time if cuBLAS splits a multiplication into more than one kernel; with a single run, the StableMeasurement flag is undefined, because one sample has no spread. Copy CodeCopiedUse a different Browserdef show(df, cols, scale=None, fmt="{:,.2f}"): out = df[cols].copy() for c, f in (scale or {}).items(): out[c] = out[c] * f print(textwrap.indent(out.to_string(index=False, float_format=lambda v: fmt.format(v)), " ")) @nsight.analyze.kernel(configs=[1 << 14, 1 < None: x = torch.randn(n, device="cuda") y = torch.randn(n, device="cuda") a = torch.randn(2048, 2048, device="cuda") with nsight.annotate("vector add"): _ = x + y with nsight.annotate("matmul 2048"): _ = a @ a @section("3. How many runs? Spread, confidence intervals and the stability flag") def stability(): df = noisy_or_not().to_dataframe() print(" 20 runs per configuration; times in microseconds:n") show(df, ["Annotation", "n", "AvgValue", "StdDev", "CI95_Lower", "CI95_Upper", "RelativeStdDevPct", "StableMeasurement"], scale={"AvgValue": 1e-3, "StdDev": 1e-3, "CI95_Lower": 1e-3, "CI95_Upper": 1e-3}) tiny = df[(df.Annotation == "vector add") & (df.n == 1 << 14)].iloc[0] print(f"n StableMeasurement is simply RelativeStdDevPct < 2. The smallest kernel ({tiny['AvgValue'] / 1e3:.1f} us)" f" varies by {tiny['RelativeStdDevPct']:.1f}%:") print(" at microsecond scale, launch and clock jitter are a large share of the time. Judge small kernels") print(" with more runs, and compare their confidence intervals rather than single averages.") return f"16K-element add RSD {tiny['RelativeStdDevPct']:.1f}%; " + f"2048 matmul RSD {df[df.Annotation == 'matmul 2048'].RelativeStdDevPct.max():.1f}%" stability() A single measurement says nothing about noise, so the next step runs each configuration twenty times. The DataFrame then carries the mean, the standard deviation, a 95 percent confidence interval, the relative standard deviation in percent, and a StableMeasurement flag, which in the source is simply a relative standard deviation below two percent. We profile vector addition at two sizes, a sixteen-thousand-element one that runs in microseconds and a sixteen-million-element one, alongside a 2048 by 2048 matrix multiplication, and compare how much each varies. The practical rule is to run short kernels more times and compare confidence intervals rather than single averages. Copy CodeCopiedUse a different Browserdef make_scale_bench(cache_control, clock_control): @nsight.analyze.kernel(configs=[1 << 18, 1 < None: x = torch.randn(n, device="cuda") with nsight.annotate("x * 2"): _ = x * 2.0 return scale @section("4. Cache and clock control: two defaults that change the answer") def cache_and_clocks(): rows = [] for cache in ("all", "none"): for clock in ("none", "base"): try: df = make_scale_bench(cache, clock)().to_dataframe() except Exception as e: print(f" cache_control={cache!r}, clock_control={clock!r}: failed -> {type(e).__name__}: {str(e)[:120]}") continue t = df[df.Metric == "gpu__time_duration.sum"].set_index("n").AvgValue hz = df[df.Metric == "gpc__cycles_elapsed.avg.per_second"].set_index("n").AvgValue for n in t.index: rows.append({"cache_control": cache, "clock_control": clock, "MB": 4 * n / 2 ** 20, "time_us": t[n] / 1e3, "GB/s": 2 * 4 * n / t[n], "SM_MHz": hz[n] / 1e6}) tab = pd.DataFrame(rows) print(textwrap.indent(tab.to_string(index=False, float_format=lambda v: f"{v:,.1f}"), " ")) print("n The T4's L2 cache is 4 MB. A 1 MB input stays in L2 between runs unless caches are flushed,") print(" so cache_control='none' measures a warm-cache kernel and 'all' (the default) a cold one.") print(" clock_control='base' locks the SM clock to base frequency for every replay (the plain ncu CLI") print(" default); nsight-python defaults to 'none', so the GPU boosts. SM_MHz is measured per kernel") print(" (gpc__cycles_elapsed.avg.per_second). The ComputeClock column is not: it is the device's") print(" rated clock attribute and stays the same whatever the kernel ran at.") globals()["CLOCK_TABLE"] = tab small = tab[tab.MB tuple[float, str]: return 2 * n ** 3 / (time_ns * 1e-9) / 1e12, "TFLOP/s" def make_precision_body(): def precision(n: int) -> None: # a fresh function object for each decorator a = torch.randn(n, n, device="cuda") b = torch.randn(n, n, device="cuda") a16, b16 = a.half(), b.half() with nsight.annotate("fp32"): _ = a @ b with nsight.annotate("fp16"): _ = a16 @ b16 return precision precision_tflops = nsight.analyze.kernel(configs=SIZES, runs=5, derive_metric=tflops, thermal_mode="off", verbosity=SILENT, combine_kernel_metrics=ADD)(make_precision_body()) precision_ratio = nsight.analyze.kernel(configs=SIZES, runs=5, normalize_against="fp32", thermal_mode="off", verbosity=SILENT, combine_kernel_metrics=ADD)(make_precision_body()) @section("5. A size sweep, a derived TFLOP/s metric, and normalisation") def precision_sweep(): df = precision_tflops().to_dataframe() tf = df[df.Metric == "tflops"].pivot(index="n", columns="Annotation", values="AvgValue") print(" derive_metric=tflops receives (gpu__time_duration.sum, n) and returns (value, 'TFLOP/s'):n") print(textwrap.indent(tf[["fp32", "fp16"]].to_string(float_format=lambda v: f"{v:,.2f}"), " ")) print("n T4 datasheet peaks: 8.1 TFLOP/s fp32, 65 TFLOP/s fp16 on tensor cores.") rd = precision_ratio().to_dataframe() fp16 = rd[rd.Annotation == "fp16"][["n", "AvgValue", "StdDev", "Unit", "Normalized"]] print("n normalize_against='fp32' divides each AvgValue by the fp32 AvgValue of the same configuration:n") print(textwrap.indent(fp16.to_string(index=False, float_format=lambda v: f"{v:,.3f}"), " ")) print("n Read the row carefully: AvgValue is now a ratio (fp16 time / fp32 time), but StdDev and Unit are") print(" still those of the raw nanosecond measurement, because only AvgValue is divided.") best = tf.loc[4096] return f"n=4096: fp32 {best['fp32']:.2f}, fp16 {best['fp16']:.2f} TFLOP/s" precision_sweep() Next, we sweep the matrix size from 512 to 4096 and run both fp32 and fp16 in one benchmark function, with one annotation per precision. A derive_metric function receives the measured time, followed by the benchmark’s arguments, and returns a value with its unit, here TFLOP/s, which the DataFrame stores as an extra metric next to the time. The T4’s datasheet peaks are 8.1 TFLOP/s in fp32 and 65 TFLOP/s in fp16 on its tensor cores, which gives the measured numbers a scale. We then run the same sweep with normalize_against set to the fp32 annotation, which divides every AvgValue by the fp32 value of the same configuration. As the source of the transformation shows, only AvgValue is divided: StdDev, the confidence interval, and the Unit column still describe the raw nanosecond measurement, which is easy to misread. Copy CodeCopiedUse a different BrowserBOUND_METRICS = ["gpu__time_duration.sum", "sm__throughput.avg.pct_of_peak_sustained_elapsed", "dram__throughput.avg.pct_of_peak_sustained_elapsed"] @nsight.analyze.kernel(metrics=BOUND_METRICS, runs=3, thermal_mode="off", verbosity=SILENT) def what_limits(n: int) -> None: a = torch.randn(n, n, device="cuda").half() x = torch.randn(64 * 2 ** 20, device="cuda") s = torch.randn(8192, 1024, device="cuda") with nsight.annotate("fp16 matmul 4096"): _ = a @ a with nsight.annotate("x * 2 (256 MB)"): _ = x * 2.0 with nsight.annotate("softmax 8192x1024"): _ = torch.softmax(s, dim=-1) @section("6. Compute-bound or memory-bound? Three metrics from one replayed kernel") def bound(): df = what_limits(4096).to_dataframe() tab = df.pivot(index="Annotation", columns="Metric", values="AvgValue") tab = tab.rename(columns={"gpu__time_duration.sum": "ms", "sm__throughput.avg.pct_of_peak_sustained_elapsed": "SM % of peak", "dram__throughput.avg.pct_of_peak_sustained_elapsed": "DRAM % of peak"}) tab["ms"] = tab["ms"] / 1e6 tab["limited by"] = np.where(tab["SM % of peak"] > tab["DRAM % of peak"], "compute", "memory") print(textwrap.indent(tab.to_string(float_format=lambda v: f"{v:,.2f}"), " ")) x2 = tab.loc["x * 2 (256 MB)"] gbs = 2 * 4 * 64 * 2 ** 20 / (x2["ms"] * 1e6) print(f"n Cross-check: x * 2 moves 2 x 256 MB in {x2['ms']:.2f} ms = {gbs:.0f} GB/s, against the T4's 320 GB/s") print(f" datasheet peak ({100 * gbs / 320:.0f}%); Nsight Compute reports {x2['DRAM % of peak']:.0f}% DRAM throughput.") print(" Collecting several metrics makes Nsight Compute replay each kernel once per pass it needs;") print(" nsight-python still returns one row per annotation and metric.") return f"x*2: {gbs:.0f} GB/s ({x2['DRAM % of peak']:.0f}% DRAM); fp16 matmul SM {tab.loc['fp16 matmul 4096', 'SM % of peak']:.0f}%" bound() Hardware counters tell us why a kernel takes as long as it does. Passing a list of metrics makes Nsight Compute replay each kernel as many times as it needs to collect them all, and Nsight Python still returns one row per annotation and metric. We collect the duration, the SM throughput, and the DRAM throughput, each as a percentage of the sustained peak, for three kernels of different character: an fp16 matrix multiplication, a scaling of a 256 MB vecto,r and a softmax over 8192 rows of 1024 values, and label each by whichever throughput is higher. For vector scaling, we also compute the effective bandwidth from the bytes it must move and the measured time, and compare it against the T4’s 320 GB/s datasheet peak and the DRAM percentage Nsight Compute reports. Copy CodeCopiedUse a different BrowserBYTES = ["gpu__time_duration.sum", "dram__bytes_read.sum", "dram__bytes_write.sum"] @nsight.analyze.kernel(metrics=BYTES, runs=5, thermal_mode="off", verbosity=SILENT, combine_kernel_metrics=lambda x, y: x + y) def fusion(n: int) -> None: a, b, c = (torch.randn(n, device="cuda") for _ in range(3)) with nsight.annotate("a + b * c (unfused)"): _ = a + b * c with nsight.annotate("torch.addcmul (fused)"): _ = torch.addcmul(a, b, c) @nsight.analyze.kernel(runs=1, thermal_mode="off", verbosity=SILENT) def fusion_without_combine(n: int) -> None: a, b, c = (torch.randn(n, device="cuda") for _ in range(3)) with nsight.annotate("a + b * c"): _ = a + b * c @section("7. Kernel fusion, measured in bytes: combine_kernel_metrics") def fused(): n = 32 * 2 ** 20 try: fusion_without_combine(n) print(" (the unfused expression profiled without combine_kernel_metrics)") except Exception as e: print(f" Profiled without combine_kernel_metrics, the unfused expression raises:n {type(e).__name__}: {str(e).splitlines()[0][:200]}n") df = fusion(n).to_dataframe() tab = df.pivot(index="Annotation", columns="Metric", values="AvgValue") tab = pd.DataFrame({"ms": tab["gpu__time_duration.sum"] / 1e6, "MB read": tab["dram__bytes_read.sum"] / 2 ** 20, "MB written": tab["dram__bytes_write.sum"] / 2 ** 20}) print(f" {n:,} float32 elements per tensor (128 MB each); metrics summed over the kernels in each region:n") print(textwrap.indent(tab.to_string(float_format=lambda v: f"{v:,.1f}"), " ")) u, f = tab.loc["a + b * c (unfused)"], tab.loc["torch.addcmul (fused)"] print(f"n The unfused version writes b * c to memory and reads it back: {u['MB read'] + u['MB written']:,.0f} MB of") print(f" traffic against {f['MB read'] + f['MB written']:,.0f} MB, and {u['ms'] / f['ms']:.2f}x the time.") return f"fusion: {u['ms'] / f['ms']:.2f}x faster, {(u['MB read'] + u['MB written']) / (f['MB read'] + f['MB written']):.2f}x less traffic" fused() Kernel fusion is usually described in terms of time; here we measure it in bytes. The expression a + b * c in eager PyTorch materializes b * c in memory before adding a, while torch.addcmul computes the same result in one operation. We first profile the unfused expression without combine_kernel_metrics to show the error Nsight Python raises when an annotated region contains more than one kernel, then profile both versions with combine_kernel_metrics summing the duration and the DRAM bytes read and written over every kernel in each region, on tensors of 2^25, about 33.5 million, float32 elements. Copy CodeCopiedUse a different Browserimport pynvml pynvml.nvmlInit() NVML = pynvml.nvmlDeviceGetHandleByIndex(0) @nsight.experimental.collect(scope=nsight.CollectionScope.ONCE) def driver_version(*args): return pynvml.nvmlSystemGetDriverVersion() @nsight.experimental.collect(scope=nsight.CollectionScope.RUN) def sm_clock_mhz(*args): return pynvml.nvmlDeviceGetClockInfo(NVML, pynvml.NVML_CLOCK_SM) @nsight.experimental.collect(scope=nsight.CollectionScope.RUN) def gpu_temp_c(*args): return pynvml.nvmlDeviceGetTemperature(NVML, pynvml.NVML_TEMPERATURE_GPU) def make_heat_body(): def heat(n: int) -> None: a = torch.randn(n, n, device="cuda").half() with nsight.annotate("fp16 matmul"): _ = a @ a return heat @section("8. Custom info collectors and thermal control (experimental API)") def collectors(): out = {} for mode in ("off", "auto"): bench = nsight.analyze.kernel(configs=[4096], runs=30, thermal_mode=mode, thermal_timeout=60, info_collectors=[driver_version, sm_clock_mhz, gpu_temp_c], verbosity=SILENT, output_prefix=f"/content/nspy_thermal_{mode}/", output_csv=True)(make_heat_body()) t0 = time.time() try: df = bench().to_dataframe() except Exception as e: print(f" thermal_mode={mode!r}: failed -> {type(e).__name__}: {str(e).splitlines()[0][:150]}") continue wall = time.time() - t0 csvs = sorted(glob.glob(f"/content/nspy_thermal_{mode}/profiled_data-*.csv")) raw = pd.read_csv(csvs[-1]) if csvs else pd.DataFrame() out[mode] = (df.iloc[0], wall, raw) r = df.iloc[0] print(f" thermal_mode={mode!r}: {r['AvgValue'] / 1e6:.3f} ms +/- {r['RelativeStdDevPct']:.1f}% " f"wall {wall:.0f}s driver {r['driver_version']}") print(f"n The collectors became DataFrame columns; RUN-scoped ones were called before every run.") if "off" not in out: return "info-collector run failed" raw = out["off"][2] tcol = "Value" if "Value" in raw.columns else raw.columns[-1] print(f" raw per-run CSV columns: {list(raw.columns)}") if {"sm_clock_mhz", "gpu_temp_c"} None: a = torch.randn(n, n, device="cuda") b = torch.randn(n, n, device="cuda") with nsight.annotate("matmul"): _ = a @ b @section("9. The experimental CUPTI backend, in its own process, against Nsight Compute") def cupti_vs_ncu(): t0 = time.time() ncu_df = matmul_ncu().to_dataframe().set_index("n") ncu_wall = time.time() - t0 if not CUPTI_EXTRA: return f"cupti-python could not be installed; Nsight Compute run took {ncu_wall:.0f}s" with open("/content/cupti_matmul.py", "w") as f: f.write(CUPTI_SCRIPT) env = {k: v for k, v in os.environ.items() if k not in ("NVTX_INJECTION64_PATH", "NV_COMPUTE_PROFILER_PERFWORKS_DIR", "NV_COMPUTE_PROFILER_IN_PROCESS")} env["PYTHONPATH"] = CUPTI_DIR + os.pathsep + env.get("PYTHONPATH", "") p = subprocess.run([sys.executable, "/content/cupti_matmul.py"], capture_output=True, text=True, env=env, timeout=900) if p.returncode != 0: print(f" CUPTI run failed (exit {p.returncode}); last lines of its stderr:") print(textwrap.indent("n".join(p.stderr.strip().splitlines()[-8:]), " ")) return f"CUPTI backend failed (exit {p.returncode}); Nsight Compute run took {ncu_wall:.0f}s" res = next((json.loads(line) for line in reversed(p.stdout.strip().splitlines()) if line.startswith('{"wall"')), None) if res is None: return f"CUPTI run printed no result; Nsight Compute run took {ncu_wall:.0f}s" cu = pd.DataFrame(res["rows"]).set_index("n") print(f" {'n':>5s} {'ncu ms':>8s} {'cupti ms':>9s} {'ratio':>6s} {'ncu RSD%':>9s} {'cupti RSD%':>11s}") for n in ncu_df.index: a, b = ncu_df.loc[n, "AvgValue"] / 1e6, cu.loc[n, "AvgValue"] / 1e6 print(f" {n:5d} {a:8.3f} {b:9.3f} {b / a:6.2f} {ncu_df.loc[n, 'RelativeStdDevPct']:9.1f} {cu.loc[n, 'RelativeStdDevPct']:11.1f}") print(f"n wall time for 4 sizes x 10 runs: Nsight Compute {ncu_wall:.1f}s, CUPTI {res['wall']:.1f}s" f" ({ncu_wall / res['wall']:.1f}x)") print(" The two tools are mutually exclusive in one process and Nsight Compute cannot be unloaded, so") print(" CUPTI runs in a child process with NSPY_NCU_INIT_AT_IMPORT=0. CUPTI records one execution per") print(" launch, with no replay, no cache flush and no clock control: cheaper, and warmer caches.") return f"CUPTI {ncu_wall / res['wall']:.1f}x faster to profile; durations within {abs(1 - cu.AvgValue.div(ncu_df.AvgValue)).max():.0%}" cupti_vs_ncu() The CUPTI backend is the other headline feature of 1.1: kernel durations measured through CUPTI’s activity API, without Nsight Compute. The two tools are mutually exclusive within a single process. Nsight Compute cannot be unloaded once active, so we write a small script that sets NSPY_NCU_INIT_AT_IMPORT to 0 before importing nsight, activates the CUPTI tool, profiles the same four matrix sizes, and runs it in a child process with the cupti-python directory on its path. CUPTI supports only the duration metric and records one execution per launch, with no replay, cache flushing, or clock control, so we compare both the durations and the wall-clock cost of profiling against the same benchmark under Nsight Compute. Copy CodeCopiedUse a different Browser@section("10. Keep the evidence: CSVs and the full Nsight Compute report") def evidence(): files = sorted(glob.glob("/content/nspy_thermal_off/*")) for f in files: print(f" {os.path.basename(f):60s} {os.path.getsize(f) / 1024:9.1f} KB") import ncu_report rep_path = next((f for f in files if f.endswith(".ncu-rep")), None) if rep_path is None: raise RuntimeError("no .ncu-rep in /content/nspy_thermal_off/ (step 8 did not complete)") rep = ncu_report.load_report(rep_path) actions = [rep.range_by_idx(i).action_by_idx(j) for i in range(rep.num_ranges()) for j in range(rep.range_by_idx(i).num_actions())] a0 = actions[0] print(f"n the .ncu-rep holds {len(actions)} profiled kernel launches; the first is") print(f" {a0.name()}") print(f" gpu__time_duration.sum = {a0['gpu__time_duration.sum'].value():,.0f} {a0['gpu__time_duration.sum'].unit()}") print(f" {len(a0.metric_names())} metrics available in the report for this launch") print("n output_csv=True writes profiled_data (one row per run) and processed_data (the aggregated") print(" DataFrame). The .ncu-rep opens in the Nsight Compute GUI for source-level analysis.") return f"{len(actions)} launches in the report, {len(a0.metric_names())} metrics each" evidence() Every profiled call leaves its evidence behind. With an output prefix and output_csv, the directory holds the Nsight Compute report, its log, the per-run profiled_data CSV and the aggregated processed_data CSV. We list the files and open the report with ncu_report, Nsight Compute’s Python report interface, which Nsight Python itself uses: it lists the profiled kernel launches, and each one carries every metric that was collected. The same .ncu-rep file opens in the Nsight Compute interface for source-level analysis. Copy CodeCopiedUse a different Browserbanner("SUMMARY") for name, res in RESULTSitems(): print(f" {name:<76s} {res}") print(""" What to carry over - import nsight first; it loads Nsight Compute into the process before CUDA starts. - Annotate exactly the work you mean to measure; one kernel per region unless you pass combine_kernel_metrics. - Decide cache and clock control on purpose: nsight-python flushes caches and leaves clocks unlocked by default, which is not what a plain ncu run does. - Use runs and the CI95 / RelativeStdDevPct columns; one run tells you nothing about noise. - After normalize_against, only AvgValue is a ratio. Where to go next - Triton and cuTile kernels: examples/07_triton_minimal.py and examples/12_cutile.py. - Plots: @nsight.analyze.plot(...) on top of @nsight.analyze.kernel, with row/col panels. - Docs: docs.nvidia.com/nsight-python Code: github.com/NVIDIA/nsight-python """) The summary prints the one-line result of each step and the habits worth keeping: import nsight before anything initializes CUDA, annotate exactly the work you mean to measure, choose cache and clock control deliberately, use enough runs and read the confidence intervals, and remember that only AvgValue is normalized. It ends with pointers to the Triton and cuTile examples in the repository and to the plotting decorator. In conclusion, Nsight Python turns kernel profiling from a separate command-line session into a few lines in the code being measured: a decorator, an annotation, and a DataFrame that carries the measurement, its spread, and its context. Working through it on a Colab T4 also shows where the convenience hides decisions that change the numbers. The profiler has to be newer than the one Colab ships and must be loaded before CUDA starts. Cache flushing and clock locking default to the opposite of what a plain ncu run does; the clock column reports the card’s rating rather than the clock the kernel ran at; normalization divides only the average. Every annotated region is expected to hold exactly one kernel unless you say how to combine them. Once those choices are explicit, the same few lines answer the questions that matter in practice: how stable a timing is, what limits a kernel, what fusion actually saves, and what a cheaper profiler such as the new CUPTI backend gives up for its lower overhead. Check out the FULL CODES here. All credit goes to the researcher of this project. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well. The post NVIDIA Nsight Python Tutorial: How to Profile GPU Kernels appeared first on MarkTechPost.

AI

Recursive Releases Flint, an Open-Weight 40B MoE Local Coding Model for Agents with 2.5B Active Params

Recursive Superintelligence has released Flint, its first model. The Recursive Flint local coding model is a mixture-of-experts (MoE) model with 40B total and 2.5B active parameters. Recursive team states that its Eureka Machine, an automated research system, helped build nearly every part of the stack. The weights are on Hugging Face under Apache 2.0. TL;DR Size: 39.8B total, about 2.5B active per token. Context: 262,144 tokens documented on the model card; the blog cites up to 512k. Runs on: 48 GB Apple Silicon Macs (M4 Pro, M5 Pro, M5 Max) or 48 GB NVIDIA GPUs. 4-bit weights take about 23 GB. Performance: Tops Recursive’s chart among MoE models under 50B on agentic coding. Trails the strongest dense models. Best: IFEval 95.1%. SWE-bench Verified 74.2%. Worst: Terminal-Bench 2.1 at 54.9%, versus 73.0% for dense Qwen 3.8. What is Recursive Flint? Flint is a decoder-only MoE Transformer for coding agents. The model investigates codebases, fixes bugs, implements features and writes tests. It is text-only and supports reasoning and OpenAI-compatible tool calls. How does Flint’s architecture work? Flint has 49 layers: 2 dense layers, then 47 MoE layers. Each MoE layer has 256 routed experts. A router selects 6 per token, plus 1 always-active shared expert. Attention mixes 2 types. 35 layers use a 513-token sliding window. 14 layers use full attention with no positional encoding. There are 32 query heads and 4 key/value heads. The 2.5B active figure includes the output head. The active body alone is about 2.2B parameters. How did the Eureka Machine build Flint? The Eureka Machine proposes ideas, implements them, tests them and picks what to try next. Recursive highlights 3 contributions: Architecture: It started from a 35B-total, 3B-active baseline. It searched expert configurations and widths, landing on 40B total and 2.5B active. Quantization: It quantized the output layer (lm_head) to 4 bits, against common practice. An offline distillation stage, with the 16-bit model as teacher, recovered accuracy. This nearly halved training steps versus a quantization-aware training baseline. The NVFP4 model scores within 0.7 points of the 16-bit model. Kernels: It wrote specialized kernels for low-bit projections, MoE operations, attention and prompt processing. On M4 and M5 chips, a Metal megakernel fuses the entire decode forward pass. How does Flint perform on coding benchmarks? All scores below are self-reported by Recursive. Sampling used temperature 1.0, top-p 0.95 and top-k 20. Terminal-Bench used the Terminus 2 harness. Solutions found through web retrieval counted as failures. SWE-bench runs used mini-swe-agent with no internet access. Agentic scores average pass@1 over at least 5 attempts. In Recursive’s chart, Qwen3.6 scores 73.4% on SWE-bench Verified. Dense Qwen 3.8 scores 73.0% on Terminal-Bench 2.1, well above Flint. Recursive’s own comparison chart (scores in %, † measured by Recursive, others developer-reported): window.addEventListener("message",function(e){if(e.data&&e.data.mtpFlintH){var f=document.getElementById("mtp-flint-frame");if(f&&e.source===f.contentWindow){f.style.height=e.data.mtpFlintH+"px";}}}); How does Flint compare with Qwen3.6, Laguna XS 2.1 and Gemma 4? Competitor figures below come from each developer’s own model card, not Recursive’s chart. How fast is Flint, and what hardware does it need? Recursive reports more than 125 tokens per second on an M4 Pro MacBook Pro. It reports more than 150 tokens per second on a 64 GB NVIDIA RTX Spark. Both are short-context figures, up to 2x faster than comparable models. It also claims Flint runs more than 5x faster than dense 25-40B models with similar coding scores. There are 3 local options: Apple Silicon: Flint INT4 MLX weights take about 22.6 GB. The rec engine needs an M4 Pro, M5 Pro or M5 Max with 48 GB memory. It reports 1.4 to 2.6x the throughput of llama.cpp on an M4 Pro. NVIDIA: The NVFP4 GGUF file is 22.9 GB. Recursive recommends at least 48 GB of GPU memory. Server: The 80 GB BF16 checkpoint needs 1 B300 or H200 GPU per replica. Flint also works with llama.cpp, Ollama, Unsloth, LM Studio, vLLM and SGLang. Key Takeaways Flint is a 40B MoE coding model with only 2.5B active parameters. It scores 74.2% on SWE-bench Verified and 54.9% on Terminal-Bench 2.1. 4-bit weights run in about 23 GB on a 48 GB Mac or GPU. Recursive’s Eureka Machine helped design the architecture, quantization and kernels. SWE-bench Pro v2 scores are Recursive-measured and not comparable to public SWE-Bench Pro. Check out the technical details, model weights and rec engine on GitHub. All credit goes to the researcher of this project. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well. The post Recursive Releases Flint, an Open-Weight 40B MoE Local Coding Model for Agents with 2.5B Active Params appeared first on MarkTechPost.

AI

Vercel Open-Sources durable-harness for Crash-Proof Coding Agents

Vercel has released durable-harness, an open-source package that keeps coding agent sessions alive through crashes and timeouts. Vercel durable-harness wraps existing harnesses, including Pi, OpenCode, Claude Code and Codex, without changing them. Each session lives in a durable log. A dead function no longer means lost work. For teams moving agents off laptops and into the cloud, that closes a real gap. TL;DR Size: npm package, version 0.0.x, Apache-2.0. Adapters cover 5 harnesses: Pi, OpenCode, fx, Claude Code, Codex. Runs on: Vercel Functions plus Vercel Queues, Node.js 22+ with Postgres, or laptop files. Claude Code and Codex need a Vercel Sandbox. Performance: No latency or throughput benchmarks published. All recovery claims are self-reported by Vercel. Best: Finished model and tool calls are read back from the log, never re-run or billed twice. Worst: Experimental 0.0.x API. Claude Code and Codex run only in a Vercel Sandbox, even self-hosted. What is durable-harness? durable-harness is a set of adapters from Vercel that store a harness session in a Workflow SDK World. A World bundles storage, a queue and a stream behind one interface. The package uses the same machinery that powers Vercel’s Workflow SDK. Why do coding agents lose work in the cloud? Harnesses like Pi and Claude Code were designed as long-lived local processes. They keep the session in memory. Locally, the workspace and history sit on the same disk, so resuming takes 1 prompt. In the cloud, a VM with persistent storage does not track which calls completed. Vercel team notes a failed turn or network error then means lost work. How does durable-harness work? It writes each step of a turn to a durable log. Before a model or tool call runs, it records an intent. When the call finishes, it records the outcome. Each turn is backed by a message on Vercel Queues. If the function dies, the queue hands the turn to a new function. That function reads the log and continues. After a crash, the log shows what finished, what was running and what never started: Completed calls are read from the log, so finished model tokens are not billed again. replay: 'safe' tools, such as lookups, can run again. replay: 'never' tools, such as refunds, never run twice. The model is told the call may have happened. Harness session files are mirrored into the log as well. That covers Claude Code’s transcript, Codex’s rollout and OpenCode’s event store. Where do files and shell commands run? Model-chosen commands never run on the function’s own disk. The tools docs list 3 workspace options: virtual() keeps files in the session log, with a bash-compatible shell and no network. It holds about 500 files. virtual({ durable: false }) keeps scratch files in memory and clears them every turn. sandbox() gives each session a Vercel Sandbox, with /workspace on a Drive. Typed JSON documents, such as a to-do list, also live in the log. Each holds at most 64 KB. <iframe id="mtp-dh-frame" title="Vercel durable-harness interactive explainer" style="width:100%;height:600px;border:0;display:block;background:transparent;" scrolling="no" loading="lazy" srcdoc="<!doctype html> <title>durable-harness explainer</title> html,body{margin:0!important;padding:0!important;background:transparent!important;} #mtp-dh{--bg:#000;--card:#0a0a0a;--line:#262626;--mute:#a1a1a1;--txt:#ededed;--blue:#0070f3;--ok:#50e3c2;--warn:#f5a623;--err:#ee0000;--mtp:#76B900; max-width:880px;margin:0 auto;background:var(--bg)!important;color:var(--txt)!important;border:1px solid var(--line)!important;border-radius:14px; font-family:Inter,-apple-system,"Segoe UI",Roboto,sans-serif;font-size:15px;line-height:1.5;overflow:hidden;box-sizing:border-box} #mtp-dh *{box-sizing:border-box} #mtp-dh hr,#mtp-dh p:empty,#mtp-dh del,#mtp-dh s{display:none!important} #mtp-dh .hd{padding:20px 24px 14px;border-bottom:1px solid var(--line)!important;background:radial-gradient(600px 160px at 0% 0%,rgba(0,112,243,.18),transparent)!important} #mtp-dh .tag{display:inline-flex;gap:6px;align-items:center;font-size:12px;color:var(--mute)!important;letter-spacing:.04em;text-transform:uppercase} #mtp-dh .tri{width:8px;height:8px;border-radius:50%;background:#0070f3!important;box-shadow:0 0 8px #0070f3} #mtp-dh h3{margin:6px 0 4px!important;font-size:22px!important;color:#fff!important;font-weight:700;letter-spacing:-.02em} #mtp-dh .sub{color:var(--mute)!important;font-size:14px;margin:0} #mtp-dh .tabs{display:flex;gap:4px;padding:10px 16px 0;border-bottom:1px solid var(--line)!important;overflow-x:auto} #mtp-dh .tab{background:transparent!important;color:var(--mute)!important;border:0!important;border-bottom:2px solid transparent!important;padding:9px 12px;font:600 13px Inter,sans-serif;cursor:pointer;white-space:nowrap} #mtp-dh .tab.on{color:#fff!important;border-bottom-color:#fff!important} #mtp-dh .pane{display:none;padding:20px 24px 22px;animation:dhfade .35s ease} #mtp-dh .pane.on{display:block} @keyframes dhfade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}} #mtp-dh .cols{display:grid;grid-template-columns:1fr 1fr;gap:14px} #mtp-dh .box{background:var(--card)!important;border:1px solid var(--line)!important;border-radius:10px;padding:14px} #mtp-dh .box h4{margin:0 0 10px!important;font-size:13px!important;color:#fff!important;display:flex;justify-content:space-between;align-items:center} #mtp-dh .pill{font-size:11px;padding:2px 8px;border-radius:99px;border:1px solid var(--line)!important;color:var(--mute)!important;font-weight:600} #mtp-dh ol.steps{list-style:none;margin:0;padding:0} #mtp-dh ol.steps li{display:flex;gap:8px;align-items:center;padding:6px 8px;border-radius:6px;font-size:13px;color:var(--mute)!important;transition:all .3s} #mtp-dh ol.steps li .k{font:600 11px ui-monospace,Menlo,monospace;width:46px;color:#888!important} #mtp-dh ol.steps li.run{background:rgba(0,112,243,.15)!important;color:#fff!important} #mtp-dh ol.steps li.done{color:#fff!important} #mtp-dh ol.steps li.done::after{content:"2713";margin-left:auto;color:var(--ok)!important} #mtp-dh ol.steps li.rerun::after{content:"ran twice";margin-left:auto;color:var(--err)!important;font-size:11px;font-weight:600} #mtp-dh ol.steps li.fromlog::after{content:"from log";margin-left:auto;color:var(--ok)!important;font-size:11px;font-weight:600} #mtp-dh ol.steps li.dead{background:rgba(238,0,0,.12)!important;color:#ff6b6b!important} #mtp-dh .stat{display:flex;justify-content:space-between;font-size:12px;color:var(--mute)!important;margin-top:10px;border-top:1px dashed var(--line)!important;padding-top:8px} #mtp-dh .stat b{color:#fff!important} #mtp-dh .btns{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0 4px} #mtp-dh button.b{background:#fff!important;color:#000!important;border:0!important;border-radius:8px;padding:8px 14px;font:600 13px Inter,sans-serif;cursor:pointer} #mtp-dh button.b.ghost{background:transparent!important;color:#fff!important;border:1px solid var(--line)!important} #mtp-dh button.b.red{background:var(--err)!important;color:#fff!important} #mtp-dh button.b:disabled{opacity:.4;cursor:not-allowed} #mtp-dh .note{font-size:12.5px;color:var(--mute)!important;margin-top:10px} #mtp-dh .log{font:12px/1.6 ui-monospace,Menlo,monospace;background:#050505!important;border:1px solid var(--line)!important;border-radius:8px;padding:10px;height:150px;overflow:hidden;margin-top:12px} #mtp-dh .log div{animation:dhfade .3s ease;color:#bbb!important} #mtp-dh .log .i{color:var(--warn)!important}#mtp-dh .log .o{color:var(--ok)!important}#mtp-dh .log .x{color:#ff6b6b!important} #mtp-dh .rules{display:grid;grid-template-columns:1fr 1fr;gap:14px} #mtp-dh .tool{cursor:pointer;transition:border-color .2s} #mtp-dh .tool:hover,#mtp-dh .tool.sel{border-color:#fff!important} #mtp-dh code,#mtp-dh pre{font-family:ui-monospace,Menlo,monospace!important;background:#111!important;color:#ededed!important;border:1px solid var(--line)!important;border-radius:5px;padding:1px 5px;font-size:12px} #mtp-dh pre{padding:10px 12px;white-space:pre-wrap;margin:10px 0 0;line-height:1.55} #mtp-dh .verdict{margin-top:14px;padding:12px;border-radius:8px;border:1px solid var(--line)!important;background:var(--card)!important;font-size:13.5px;min-height:60px} #mtp-dh .ws{display:grid;grid-template-columns:repeat(3,1fr);gap:12px} #mtp-dh .ws .box{cursor:pointer} #mtp-dh .ws .box.sel{border-color:var(--blue)!important;box-shadow:0 0 0 1px var(--blue)} #mtp-dh .meter{height:8px;background:#1a1a1a!important;border-radius:99px;overflow:hidden;margin:6px 0 2px} #mtp-dh .meter i{display:block;height:100%;width:0;background:var(--blue)!important;transition:width .9s cubic-bezier(.2,.8,.2,1)} #mtp-dh .ml{font-size:11px;color:var(--mute)!important;display:flex;justify-content:space-between} #mtp-dh table{width:100%;border-collapse:collapse;font-size:12.5px} #mtp-dh th,#mtp-dh td{border-bottom:1px solid var(--line)!important;padding:8px 6px;text-align:left;vertical-align:top;color:var(--txt)!important;background:transparent!important} #mtp-dh th{color:var(--mute)!important;font-weight:600} #mtp-dh td.y{color:var(--ok)!important}#mtp-dh td.n{color:#888!important} #mtp-dh .worlds{display:flex;gap:10px;flex-wrap:wrap} #mtp-dh .world{flex:1;min-width:150px;cursor:pointer} #mtp-dh .world.sel{border-color:#fff!important} #mtp-dh .ft{display:flex;justify-content:space-between;align-items:center;padding:10px 24px;border-top:1px solid var(--line)!important;font-size:11.5px;color:#777!important} #mtp-dh .ft b{color:var(--mtp)!important;font-weight:700} #mtp-dh a{color:#fff!important;text-decoration:underline} @media (max-width:640px){ #mtp-dh .cols,#mtp-dh .rules,#mtp-dh .ws{grid-template-columns:1fr} #mtp-dh .hd,#mtp-dh .pane{padding-left:16px;padding-right:16px} #mtp-dh h3{font-size:19px!important} #mtp-dh .ft{flex-direction:column;gap:4px;align-items:flex-start;padding:10px 16px} #mtp-dh table{font-size:11.5px} } <div id="mtp-dh"> <div class="hd"> <div class="tag"><span class="tri"></span> Vercel · durable-harness · v0.0.x</div> <h3>How durable-harness keeps agent turns alive</h3> <p class="sub">Every model and tool call goes into a durable log. A crashed turn resumes on a new function.</p> </div> <div class="tabs" role="tablist"> <button class="tab on" data-t="p1">Crash demo</button> <button class="tab" data-t="p2">Replay rules</button> <button class="tab" data-t="p3">Workspaces</button> <button class="tab" data-t="p4">Where it runs</button> <button class="tab" data-t="p5">Harnesses</button> </div> <div class="pane on" id="p1"> <div class="cols"> <div class="box"><h4>Without durability <span class="pill">state in memory</span></h4> <ol class="steps" id="sA"></ol> <div class="stat"><span>Steps run twice <b id="aRe">0</b></span><span>Model calls <b id="aMc">0</b></span></div> </div> <div class="box"><h4>With durable-harness <span class="pill">state in log</span></h4> <ol class="steps" id="sB"></ol> <div class="stat"><span>Steps run twice <b id="bRe">0</b></span><span>Model calls <b id="bMc">0</b></span></div> </div> </div> <div class="btns"> <button class="b" id="go"> Run turn</button> <button class="b red" id="kill" disabled>Kill function</button> <button class="b ghost" id="rst">Reset</button> </div> <div class="log" id="lg"><div>Press Run turn, then kill the function while CI is running.</div></div> <p class="note">Scenario from Vercel's post: an agent adds a pricing page, opens a PR, and waits for CI. Illustrative simulation, not a benchmark.</p> </div> <div class="pane" id="p2"> <p class="sub" style="margin-bottom:12px">Click a tool, then crash it mid-call.</p> <div class="rules"> <div class="box tool sel" data-r="safe"><h4><code>get_order</code><span class="pill">replay: 'safe'</span></h4><span class="note">A lookup. Safe to repeat after a crash.</span></div> <div class="box tool" data-r="never"><h4><code>refund_order</code><span class="pill">replay: 'never'</span></h4><span class="note">A refund. Must never run twice.</span></div> </div> <div class="btns"><button class="b red" id="crash">Crash mid-call</button></div> <div class="verdict" id="vd">Pick a tool and press Crash mid-call.</div> <pre id="code">{ name: 'get_order', replay: 'safe', ... }</pre> </div> <div class="pane" id="p3"> <div class="ws"> <div class="box sel" data-w="0"><h4><code>virtual()</code></h4><span class="note">Files in the session log. Bash-compatible shell, no network.</span></div> <div class="box" data-w="1"><h4><code>virtual({durable:false})</code></h4><span class="note">In-memory scratch. Cleared every turn.</span></div> <div class="box" data-w="2"><h4><code>sandbox()</code></h4><span class="note">Per-session Vercel Sandbox, <code>/workspace</code> on a Drive.</span></div> </div> <div class="box" style="margin-top:14px" id="wsd"></div> </div> <div class="pane" id="p4"> <div class="worlds"> <div class="box world sel" data-g="0"><h4>Vercel</h4><span class="note">Vercel World + Agent Runs traces</span></div> <div class="box world" data-g="1"><h4>Laptop</h4><span class="note">Local files, zero config</span></div> <div class="box world" data-g="2"><h4>Self-host</h4><span class="note">Postgres World, Node.js 22+</span></div> </div> <pre id="wc"></pre> <p class="note" id="wn"></p> </div> <div class="pane" id="p5"> <table> <tr><th>Harness</th><th>Package</th><th>Runs in</th><th>Status</th></tr> <tr><td>Pi</td><td><code>durable-harness/pi-durable</code></td><td>Your function + virtual workspace or Sandbox</td><td class="y">Supported</td></tr> <tr><td>OpenCode</td><td><code>durable-harness/opencode</code></td><td>Your function + virtual workspace or Sandbox</td><td class="y">Supported</td></tr> <tr><td>fx</td><td><code>libfx</code></td><td>Durable on its own</td><td class="y">Supported</td></tr> <tr><td>Claude Code</td><td><code>durable-harness/claude-code</code></td><td>Vercel Sandbox only</td><td class="n">Experimental</td></tr> <tr><td>Codex</td><td><code>durable-harness/codex</code></td><td>Vercel Sandbox only</td><td class="n">Experimental</td></tr> </table> <p class="note">Claude Code and Codex are single-user CLIs that keep state on disk, so each session gets its own Sandbox.</p> </div> <div class="ft"><span>Source: vercel.com/blog/making-every-harness-durable · Vercel docs · Oct 2026</span><b>© Marktechpost</b></div> </div> (function(){ var R=document.getElementById('mtp-dh'); function $(s){return R.querySelector(s)}function $$(s){return R.querySelectorAll(s)} function rz(){try{parent.postMessage({mtpEmbedHeight:R.offsetHeight+40,id:'mtp-dh'},'*')}catch(e){}} $$('.tab').forEach(function(t){t.onclick=function(){$$('.tab').forEach(function(x){x.classList.remove('on')});$$('.pane').forEach(function(x){x.classList.remove('on')});t.classList.add('on');$('#'+t.dataset.t).classList.add('on');if(t.dataset.t==='p3')ws(cur);setTimeout(rz,60)}}); var S=[['model','Plan the change',1],['read','app/page.tsx',0],['write','app/pricing/page.tsx',0],['run','gh pr create',0],['wait','CI checks on the PR',0],['model','Reply with the result',1]]; function draw(id){var o=$(id);o.innerHTML='';S.forEach(function(s){var li=document.createElement('li');li.innerHTML='<span class="k">'+s[0]+'</span>'+s[1];o.appendChild(li)})} var i=0,tm=null,killed=false,ma=0,mb=0; function log(t,c){var l=$('#lg'),d=document.createElement('div');d.className=c||'';d.textContent=t;l.appendChild(d);while(l.children.length>6)l.removeChild(l.firstChild)} function reset(){clearTimeout(tm);i=0;killed=false;ma=mb=0;draw('#sA');draw('#sB');$('#aRe').textContent=0;$('#bRe').textContent=0;$('#aMc').textContent=0;$('#bMc').textContent=0;$('#go').disabled=false;$('#kill').disabled=true;$('#lg').innerHTML='<div>Press Run turn, then kill the function while CI is running.</div>'} function li(id,n){return $(id).children[n]} function step(){if(i>=S.length){$('#kill').disabled=true;log('turn complete','o');return} ['#sA','#sB'].forEach(function(id){var e=li(id,i);e.className='run'}); log('intent '+S[i][0]+' '+S[i][1],'i'); tm=setTimeout(function(){['#sA','#sB'].forEach(function(id){li(id,i).className='done'});if(S[i][2]){ma++;mb++;$('#aMc').textContent=ma;$('#bMc').textContent=mb}log('outcome '+S[i][0]+' ok','o');i++;step()},i===4?1800:700)} $('#go').onclick=function(){reset();$('#go').disabled=true;$('#kill').disabled=false;$('#lg').innerHTML='';step()}; $('#rst').onclick=reset; $('#kill').onclick=function(){if(i>=S.length)return;clearTimeout(tm);killed=true;$('#kill').disabled=true;var k=i; li('#sA',k).className='dead';li('#sB',k).className='dead';log('function 1 died at step '+(k+1),'x'); setTimeout(function(){log('without durability: manual restart, turn starts over','x'); draw('#sA');var re=0,j=0;(function a(){if(j>=S.length){$('#aRe').textContent=re;return}var e=li('#sA',j);e.className='run';setTimeout(function(){e.className=j<k?'rerun':'done';if(j<k){re++;$('#aRe').textContent=re}if(S[j][2]){ma++;$('#aMc').textContent=ma}j++;a()},260)})(); log('with durable-harness: queue hands turn to function 2','o'); for(var q=0;q=S.length){log('turn complete, 0 steps repeated','o');return}var e=li('#sB',i);e.className='run';setTimeout(function(){e.className='done';if(S[i][2]){mb++;$('#bMc').textContent=mb}i++;b()},450)})(); },600)}; reset(); var rule='safe'; $$('.tool').forEach(function(t){t.onclick=function(){$$('.tool').forEach(function(x){x.classList.remove('sel')});t.classList.add('sel');rule=t.dataset.r;$('#vd').textContent='Pick a tool and press Crash mid-call.';$('#code').textContent=rule==='safe'?"{ name: 'get_order', replay: 'safe', ... }":"{ name: 'refund_order', replay: 'never', ... }"}}); $('#crash').onclick=function(){var v=$('#vd');v.style.opacity=0;setTimeout(function(){v.innerHTML=rule==='safe'?'<b style="color:#50e3c2">Resumed:</b> the interrupted lookup can run again. Pi reruns it; Claude Code, Codex and OpenCode tell the model it is safe to repeat.':'<b style="color:#f5a623">Never re-run:</b> the model is told the refund may or may not have happened, so it can check first. Every harness waits for the log to record a 'never' call before it starts.';v.style.transition='opacity .3s';v.style.opacity=1;rz()},150)}; var W=[{t:'Short tasks and small workspaces',f:['Files kept','In the session log'],m:[['Workspace size','about 500 files',55]],x:'A crash mid-call leaves nothing behind, and the call runs again. Cost: function CPU plus log events and storage.'}, {t:'Scratch work within a turn',f:['Files kept','Cleared every turn'],m:[['Persistence','none across turns',8]],x:'Nothing goes to the log. The model is told files are cleared. Cost: function CPU only.'}, {t:'Real projects and long-running work',f:['Files kept','On a Drive that outlives every Sandbox'],m:[['Isolation','1 Sandbox per session',100]],x:'A command a crash interrupts may have partly run, and the model is told. Cost: Sandbox and Drive usage, plus a start on first use.'}]; var cur=0;function ws(n){cur=n;var w=W[n];$('#wsd').innerHTML='<h4>Best for: '+w.t+'</h4><div class="note">'+w.f[0]+': <b style="color:#fff">'+w.f[1]+'</b></div>'+w.m.map(function(m){return '<div class="meter"><i></i></div><div class="ml"><span>'+m[0]+'</span><span>'+m[1]+'</span></div>'}).join('')+'<p class="note">'+w.x+' Typed JSON documents live in the log too, up to 64 KB each.</p>';setTimeout(function(){$$('#wsd .meter i').forEach(function(e){e.style.width=w.m[0][2]+'%'});rz()},40)} $$('.ws .box').forEach(function(b){b.onclick=function(){$$('.ws .box').forEach(function(x){x.classList.remove('sel')});b.classList.add('sel');ws(+b.dataset.w)}});ws(0); var G=[["// On Vercel: @workflow/world-vercel is picked automaticallynimport { claudeCode } from 'durable-harness/claude-code'nexport const agent = claudeCode({ model: 'anthropic/claude-sonnet-5.5' })",'Sessions show up in Agent Runs with each model and tool call. Resumed turns appear in the same trace.'], ["# Local dev: @workflow/world-local, no configurationn# sessions live in .workflow-data by defaultnnpm install durable-harness",'The local World suits development and a single server with a persistent disk.'], ["# Self-host: 2 variables move every agent to PostgresnWORKFLOW_TARGET_WORLD="@workflow/world-postgres"nWORKFLOW_POSTGRES_URL="postgres://user:password@host:5432/database"",'Needs @workflow/world-postgres 5.0.4+ and Node.js 22+. It is a reference implementation: no route auth, no encryption, not tuned for scale.']]; function wd(n){$('#wc').textContent=G[n][0];$('#wn').textContent=G[n][1];rz()} $$('.world').forEach(function(b){b.onclick=function(){$$('.world').forEach(function(x){x.classList.remove('sel')});b.classList.add('sel');wd(+b.dataset.g)}});wd(0); window.addEventListener('load',rz);window.addEventListener('resize',rz);setTimeout(rz,300); })(); "> window.addEventListener('message',function(e){if(e.data&&e.data.id==='mtp-dh'&&e.data.mtpEmbedHeight){var f=document.getElementById('mtp-dh-frame');if(f){f.style.height=e.data.mtpEmbedHeight+'px';}}}); Where can you run durable-harness? The agent code stays the same across Worlds: Vercel: the Vercel World, with every session traced in Agent Runs. Local: files on your machine, with no configuration. Self-hosted: the Postgres World, version 5.0.4 or later, on Node.js 22+. Vercel’s blog also lists MongoDB, Redis and Turso. 2 environment variables, WORKFLOW_TARGET_WORLD and WORKFLOW_POSTGRES_URL, move an agent to Postgres. How does durable-harness compare? Key Takeaways durable-harness logs every model and tool call, then resumes crashed turns on a new function. Finished calls are never repeated, so tokens are not billed twice. Pi and OpenCode run in your function; Claude Code and Codex need a Vercel Sandbox. Self-hosting works with 2 env vars and a Postgres World. It is 0.0.x and experimental, with no published benchmarks. Check out the GitHub repo, docs and Vercel’s announcement. All credit goes to the researcher of this project. Also, feel free to follow us on Twitter and don’t forget to join our 150k+ML SubReddit and Subscribe to our Newsletter. Wait! are you on telegram? now you can join us on telegram as well. The post Vercel Open-Sources durable-harness for Crash-Proof Coding Agents appeared first on MarkTechPost.

Advertisement
AI

OpenAI’s GPT-5.6 Sol and Google’s Gemini 4 Argon found critical PHP flaw affecting millions of websites

OpenAI's GPT-5.6 Sol discovered a critical PHP vulnerability as part of the Patch the Planet initiative that could affect millions of websites. Security firm Wiz independently found the same flaw using Google's Gemini 4 Argon. OpenAI warns that the same AI capabilities that find vulnerabilities can help attackers too. The article OpenAI's GPT-5.6 Sol and Google's Gemini 4 Argon found critical PHP flaw affecting millions of websites appeared first on The Decoder.

AI

Google Cloud now offers OpenAI models alongside Gemini and Claude, becoming the only major provider with all three

Google Cloud is the first major cloud provider to offer Gemini, Claude, and OpenAI's GPTs on a single platform. At launch, GPT-6 Astra, GPT-6.1 Sol, and GPT-6 Luna are available as a private preview for enterprise customers. The article Google Cloud now offers OpenAI models alongside Gemini and Claude, becoming the only major provider with all three appeared first on The Decoder.

AI

Deepmind founder Hassabis shortens AGI timeline from five to two to three years

Demis Hassabis keeps moving up his AGI timeline. The Nobel laureate and former Google Deepmind CEO now expects artificial general intelligence within two to three years, down from three to five years earlier this year. The article Deepmind founder Hassabis shortens AGI timeline from five to two to three years appeared first on The Decoder.

Advertisement
AI

Inception’s Mercury 3 diffusion model claims to beat GPT-6 Luna and Claude Haiku 5.5 at a fraction of the wait

Inception says Mercury 3 is the most capable diffusion language model released so far. The company claims it runs four to eight times faster than GPT-6 Luna and Claude Haiku 5.5 and beats both in eight of 12 benchmarks. There are no independent tests yet. The article Inception's Mercury 3 diffusion model claims to beat GPT-6 Luna and Claude Haiku 5.5 at a fraction of the wait appeared first on The Decoder.

Advertisement
AI

China protests the US Deepseek ban while blocking ChatGPT, Claude, and Gemini at home

The US has banned Deepseek from its government systems, and China is protesting sharply and demanding the ban be lifted. Yet Beijing itself blocks ChatGPT, Claude, and Gemini across its entire market. China's outrage isn't very credible, but it should be taken seriously as a political signal of possible retaliation. The article China protests the US Deepseek ban while blocking ChatGPT, Claude, and Gemini at home appeared first on The Decoder.