{
  "source_file": "0930",
  "source_platform": "Dev.to",
  "author": "Aleksei Romanov",
  "title_listed": "AsyncGRPO: Eliminating GPU Idle Bubbles in Environment-Heavy RL Post-Training",
  "keywords": "",
  "domain": "大模型算法",
  "content_type": "深度技术解析",
  "is_paid_flag": "否",
  "url": "https://dev.to/g_factor/asyncgrpo-eliminating-gpu-idle-bubbles-in-environment-heavy-rl-post-training-12c9",
  "publish_date": "2026-09-27",
  "corpus_stage": "",
  "notes": "",
  "id": "d4026d1ae4e9",
  "collected_at": "2026-10-02T13:54:34",
  "title": "AsyncGRPO: Eliminating GPU Idle Bubbles in Environment-Heavy RL Post-Training - DEV Community",
  "body_markdown": "Imagine you hire a world-class chef and pay them $50,000 a month. The chef chops ingredients with blinding speed in two seconds flat—and then stands with their arms crossed for forty-five seconds, staring at a slow kettle on the stove waiting for water to boil, refusing to touch any other dish until that single kettle whistles.\nYou would fire that kitchen manager on the spot. Yet if you look at how most reinforcement learning (RL) teams train reasoning models today on complex engineering tasks,\nthat is exactly what their GPU clusters are doing all day long\n.\nIn toy math benchmarks like GSM8K, reward verification takes less than a millisecond: you just string-match a number. In those simple setups, GPU token generation represents 95% of your wallclock time.\nIn real-world enterprise post-training, however,\nthe bottleneck is completely inverted\n. When training specialized models for synthesizable hardware design (Verilog simulation), multiphysics modeling (Modelica solvers), aerodynamic optimization (OpenFOAM CFD), or formal theorem proving (Lean 4 kernel checks):\nInference is blistering fast:\nModern engines like vLLM emit rollouts in\n1 to 2 seconds\non NVIDIA H100s.\nThe environment rollout is slow:\nRunning compilers, test suites, and simulators takes\n15 to 60+ seconds of heavy CPU work\n.\nUnder traditional synchronous Group Relative Policy Optimization (GRPO), your multi-million-dollar GPU cluster hits a global synchronization barrier and\nsits completely idle for 75% to 85% of total training time\n, burning expensive cloud allocations while waiting for CPU verifiers to finish.\nAsyncGRPO\nblows up this stop-and-wait loop. By decoupling rollout generation, concurrent gym execution, and policy updates into an asynchronous streaming pipeline, the GPUs never have to wait.\nStart here\nUnderstand how asynchronous pipelining eliminates GPU idle bubbles, how to size concurrent environment workers using queueing models, how bounded staleness preserves policy gradient correctness, and why host colocation cuts multi-gigabyte network penalties.\nEnvironment / gym:\nThe stateful task execution harness, such as a compiler sandbox, physical simulator, or test runner.\nRollout:\nA candidate trajectory sampled by the policy, containing generated actions, observations, and timestamps.\nPolicy staleness:\nThe version lag between the policy checkpoint that generated an attempt and the current training weights.\nBounded staleness:\nAn importance-sampling rule that bounds policy drift and discards trajectories older than a strict threshold.\n1. The Anatomy of the GPU Bubble\nHere is what a standard synchronous training step looks like when you graph it over time:\nSame hardware, same 38.5 seconds. The difference is whether the GPU is allowed to start the next group while the CPU is still checking the last one.\nThe Synchronous Stop-and-Wait Loop:\n[GPU: Generate 16 Rollouts (1.5s)] → [CPU: Run Simulators & Verifiers (35.0s, GPUs Idle ⏸️)] → [GPU: Compute Gradients (2.0s)]\nIn a 38.5-second loop where GPUs only work for 3.5 seconds, your GPUs are busy only about 9% of the time, and Model FLOPs Utilization (MFU), which also counts how well those busy seconds use the Tensor Cores, is lower still. You are paying full price for H100 Tensor Cores that spend most of their lives taking naps.\nWorse: GRPO introduces the\nStraggler Problem\n. To train on a prompt, the model samples a group of\nG\ncandidate trajectories (e.g. 16 completions). Even if 15 of those completions fail or pass quickly in 3 seconds, if just\none\ncompletion triggers a pathological simulator recursion, an infinite loop, or a timeout-bound unit test (e.g. 45 seconds),\nthe entire distributed cluster halts at the barrier\nuntil that single slowest test completes.\n2. AsyncGRPO: Turning Stop-and-Wait into a Continuous Factory\nAsyncGRPO breaks the lockstep by restructuring the RL loop into an asynchronous producer-consumer pipeline:\nPipeline Stage\nHardware Engine\nWhat It Actually Does\nSync Model\n1. Rollout Workers\nInference GPUs (vLLM / SGLang)\nContinuously streams candidate token rollouts from the latest policy snapshot without waiting for grading.\nNon-blocking async queues\n2. Gym Replica Pool\nHost CPU Cores / Sandboxes\nExecutes compilers, hardware simulators, test suites, and deterministic reward scoring concurrently.\nConcurrent worker pool; streams completed rewards directly to training queue\n3. Ready Buffer\nHost Memory (POSIX IPC)\nHolds fully graded groups with pre-computed advantages, ready for training.\nPriority streaming queue with staleness filters\n4. Policy Trainer\nTraining GPUs (PyTorch / Megatron)\nPulls ready batches from the queue, runs backpropagation, updates weights, and broadcasts deltas.\nContinuous backpropagation with periodic background NCCL weight sync\nHow Overlapping Keeps Silicon Hot\nCompare sequential execution against overlapped asynchronous scheduling in the timeline below:\nThree jobs (generate 2, environment 6, update 2 time units). Synchronous: done at 30. Overlapped with one gym worker: 22, the environment is the bottleneck. With the gym pool sized to T_env / T_gen = 3 workers: 14.\nThe\noriginal post\nhas an interactive 3D version of this timeline.\nWhile Gym Replica Pool A is grinding through a 30-second Verilator simulation for Batch\nN\n, the inference GPUs are already generating candidate trajectories for Batch\nN+1\n.\nSimultaneously, the training GPUs are running backward passes on Batch\nN-1\n, which just completed its verification checks a second ago.\nBy sizing the concurrent Gym Replica Pool to match the ratio of simulator time to generation time, both your CPU cores and your GPU Tensor Cores operate at\nnear-100% continuous duty cycles\n.\n3. Worker Capacity Math: Sizing the Queue\nMore queues do not magically create compute capacity. If generation produces 10 rollouts per second while your CPU verifiers can only score 2 per second, the buffer grows by 8 rollouts per second until host memory explodes or the data becomes hopelessly stale.\nWe can model the required concurrent environment worker capacity using queueing theory:\nρ\n=\nλ\nE\n[\nS\n]\nn\n\\rho=\\frac{\\lambda\\,\\mathbb{E}[S]}{n}\nρ\n=\nn\nλ\nE\n[\nS\n]\n​\nHere λ is the arrival rate in candidate attempts per second,\nE\n[\nS\n] is the average environment service time in seconds per rollout, and\nn\nis the number of concurrent worker processes. To sustain the pipeline without unbounded queue growth, utilization ρ must remain strictly below 1:\nSizing Rule of Thumb:\nAlways size the concurrent worker pool with at least 25% to 30% headroom above the arrival rate:\nn ≥ 1.3 × (T_env / T_gen) × Group_Size\nIf an H100 emits 16 rollouts in 2 seconds (λ = 8/s) and verification takes 6 seconds (\nE\n[\nS\n] = 6), you need more than 8 × 6 = 48 concurrent CPU workers just to keep up, and about 1.3 × 48 ≈ 63 with the recommended headroom.\nUtilization ρ = λ·E[S] / n for λ = 8 rollouts/s and E[S] = 6 s. Below 48 workers the queue grows without bound; about 63 gives the rule-of-thumb 30% headroom.\n4. Bounded Staleness: Keeping Policy Gradients Honest\nWhenever you make a training loop asynchronous, every reinforcement learning purist immediately asks the same question:\n“What about policy staleness? If the trainer updates weights while a slow simulator is still running, isn’t that rollout off-policy?”\nYes, it is slightly off-policy. But modern asynchronous RL systems (such as Tencent’s AReaL, ByteDance’s Relax, and Red Hat / Hugging Face TRL’s AsyncGRPO) handle this with clean mathematical rigor:\n4.1 Importance Sampling with a Strict Staleness Ceiling\nThe system tracks the policy version difference:\nΔ\nt\n=\nV\ne\nr\ns\ni\no\nn\n(\nθ\nt\nr\na\ni\nn\ne\nr\n)\n−\nV\ne\nr\ns\ni\no\nn\n(\nθ\nr\no\nl\nl\no\nu\nt\n)\n\\Delta t = \\mathrm{Version}(\\theta_{\\mathrm{trainer}}) - \\mathrm{Version}(\\theta_{\\mathrm{rollout}})\nΔ\nt\n=\nVersion\n(\nθ\ntrainer\n​\n)\n−\nVersion\n(\nθ\nrollout\n​\n)\nWe enforce a strict boundary (typically\nmax_staleness = 1\n):\nr\nt\n(\nθ\n)\n=\nπ\nθ\n(\na\nt\n∣\ns\nt\n)\nπ\nθ\nr\no\nl\nl\no\nu\nt\n(\na\nt\n∣\ns\nt\n)\nr_t(\\theta)=\\frac{\\pi_{\\theta}(a_t\\mid s_t)}{\\pi_{\\theta_{\\mathrm{rollout}}}(a_t\\mid s_t)}\nr\nt\n​\n(\nθ\n)\n=\nπ\nθ\nrollout\n​\n​\n(\na\nt\n​\n∣\ns\nt\n​\n)\nπ\nθ\n​\n(\na\nt\n​\n∣\ns\nt\n​\n)\n​\nFresh Enough (Δt ≤ 1):\nThe trainer applies standard PPO/GRPO importance sampling. Because weights are synchronized rapidly over high-speed NVLink/NCCL, the policy drift between adjacent steps is tiny, well within the safe surrogate clipping corridor\n[1 - ε, 1 + ε]\n.\nToo Stale (Δt > 1):\nIf a pathological simulator run or hung container takes too long and exceeds the staleness ceiling, the trajectory is dropped from the queue. It never touches the optimizer.\nWith max_staleness = 1, a rollout is trained on only if the trainer moved at most one policy version while it was being generated and graded.\n4.2 Intra-Trajectory Consistency\nIn multi-turn agent tasks, an episode might require multiple actions in sequence. AsyncGRPO guarantees that\nall intermediate decisions in a single episode are generated by the identical policy checkpoint\n. Weight updates are synced only at episode boundaries, so the model never suffers from policy schizophrenia within a single rollout.\n5. The Hidden Killer: Why Gyms Must Live on the Same Machine\nWhen teams first try to scale asynchronous RL, they almost always make the same architectural mistake: they set up their expensive GPU nodes in one cluster, and spin up a giant Kubernetes pool of separate CPU instances somewhere else in the VPC to run the simulators.\nIn heavy agent environments, this architecture causes a devastating\nnetwork transport disaster\n.\nThe Massive Payload of Real Tool Traces\nIn toy NLP tasks, an episode emits a 50-byte string. But real engineering environments generate massive artifacts:\nCompiler stdout/stderr logs and full stack traces (often 1 to 2 MB).\nHardware simulation signal waveform files (VCD dumps, 5 to 50 MB per rollout).\nAbstract Syntax Trees, intermediate code representations, and CAD geometry meshes.\nAcross a batch of 128 parallel rollouts, a single training step generates\n1.5 GB to 5 GB of raw ephemeral trace data\n!\nArchitecture Strategy\nHow Data Moves\nLatency Penalty per Step\nCloud Network Bill\nSerialization Overhead\nRemote Gym Cluster (TCP/IP)\n10GbE / 25GbE VPC Network\n450 ms – 1,800 ms\nSevere ($$ cross-AZ fees)\nHigh (JSON / Protobuf marshalling)\nColocated Host Gyms (Shared Node)\nPOSIX Shared Memory (\n/dev/shm\n) / IPC\n< 5 ms\nZero ($0 egress)\nZero-Copy (Memory pointer pass)\nLook at What Modern GPU Servers Actually Are\nAn 8x NVIDIA H100 SXM server is not just a box of GPUs. It is a supercomputing monster equipped with:\n128 to 256 high-performance CPU cores\n(dual AMD EPYC or Intel Xeon processors).\n1.5 to 2.0 Terabytes of high-speed DDR5 RAM\n.\n15 to 30 Terabytes of blazing NVMe SSDs\nrunning at 12+ GB/sec.\nWhen you train standard models, those 192 CPU cores and 1.5 TB of host RAM are barely doing anything! By colocating your Gym Replica Pool directly on the host CPUs of the GPU machines:\nZero Network Hops:\nvLLM hands tokens to local sandboxes via Unix Domain Sockets or shared memory.\nZero-Copy Ingestion:\nMulti-megabyte waveform dumps and compiler outputs are written directly to\n/dev/shm\n(in-memory RAM filesystem). The trainer reads them in sub-millisecond memory lookups.\nZero Cloud Egress:\nYou stop paying cloud providers thousands of dollars a month just to stream temporary simulation logs across availability zones.\n6. The Hard Numbers: What Published Benchmarks Show\nData from recent open-source asynchronous RL systems (Red Hat Async-GRPO, Tencent AReaL, ByteDance Relax, and DORA arXiv:2604.26256) confirms the massive leap in hardware efficiency:\nFramework & Workload\nEnvironment Task\nSynchronous Baseline\nAsyncGRPO Performance\nObserved Gain\nAsync-GRPO (Red Hat 2025/2026)\nMath Reasoning (DeepScaleR)\nTRL v0.16.0 (Sync): Baseline\nAsync Streaming:\n11.0x – 12.5x\n11.0x – 12.5x vs Sync TRL\nAsync-GRPO vs VERL (v0.2.0)\n8-rollout Math Reasoning\nVERL Baseline: 1.00x\nAsync-GRPO:\n1.42x\n42.4% gain over VERL\nAReaL (Tencent 2025/2026)\nCode Generation & Test Execution\nSynchronous PPO/GRPO Baseline\nDecoupled Streaming\n2.77x Training Speedup\nDORA (arXiv:2604.26256)\nIndustrial Long-Horizon Agent Tasks\nGlobal-Batch Sync Scheduling\nMulti-Version Streaming Rollout\n2.0x – 4.0x Acceleration\nThe Big Takeaways\nGPU Idle Time Drops from 76% to < 4%:\nIn benchmarks from Relax and AReaL, trainer idle ratio collapsed from 70%–80% in synchronous mode down to\n0.1%–3.5% in fully async mode\n. You get roughly 3x the training throughput on the exact same hardware.\nZero Quality Penalty:\nAs long as\nmax_staleness ≤ 1\nis enforced, final benchmark accuracy and reasoning scores track synchronous baselines perfectly.\n7. The Engineer’s Checklist for Heavy RL Gyms\nProfile First:\nMeasure your token generation time (\nT\ngen\n) against your simulator time (\nT\nenv\n). If your verifiers take more than twice as long as generation (\nT\nenv\n> 2 ×\nT\ngen\n), synchronous GRPO is actively wasting over half your GPU budget. Switch to AsyncGRPO immediately.\nColocate on the Host Node:\nRun your gym sandboxes on the host CPU cores of your GPU machines and pass data via\n/dev/shm\n. Never ship temporary simulation logs over network switches.\nSet Aggressive Timeouts:\nA single hung compiler or infinite loop in a rollout will choke your queue. Set hard execution timeouts (e.g. 2.5x median task time) and fail the trajectory immediately.\nKeep Staleness Tight:\nStart with\nmax_staleness = 1\n. Only expand to 2 if your verifiers have extreme runtime variance and your importance-sampling ratios remain stable.\nApplying this to your own models?\nWe build private RL training environments with deterministic verifiers for teams' own workflows.\nPrivate training environments\n.\nOriginally published at\ng-ftech.com\n.",
  "metadata": {
    "title": "AsyncGRPO: Eliminating GPU Idle Bubbles in Environment-Heavy RL Post-Training - DEV Community",
    "description": "How async GRPO decouples rollout generation, CPU gym execution, and policy updates to remove 80% GPU idle time in environment-heavy agent RL. Tagged with machinelearning, reinforcementlearning, llm, gpu.",
    "keywords": "machinelearning, reinforcementlearning, llm, gpu, software, coding, development, engineering, inclusive, community",
    "author": null,
    "og:type": "article",
    "og:url": "https://dev.to/g_factor/asyncgrpo-eliminating-gpu-idle-bubbles-in-environment-heavy-rl-post-training-12c9",
    "og:title": "AsyncGRPO: Eliminating GPU Idle Bubbles in Environment-Heavy RL Post-Training",
    "og:description": "How async GRPO decouples rollout generation, CPU gym execution, and policy updates to remove 80% GPU idle time in environment-heavy agent RL.",
    "og:site_name": "DEV Community",
    "og:image": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fwww.g-ftech.com%2Fog%2Fasync-grpo-gym-rollout-scaling.png",
    "twitter:site": "@thepracticaldev",
    "twitter:creator": "@",
    "twitter:title": "AsyncGRPO: Eliminating GPU Idle Bubbles in Environment-Heavy RL Post-Training",
    "twitter:description": "How async GRPO decouples rollout generation, CPU gym execution, and policy updates to remove 80% GPU idle time in environment-heavy agent RL.",
    "twitter:card": "summary_large_image",
    "twitter:widgets:new-embed-design": "on",
    "twitter:image:src": "https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fwww.g-ftech.com%2Fog%2Fasync-grpo-gym-rollout-scaling.png"
  },
  "paywall": {
    "listed_paid": false,
    "detected": false,
    "marker": "",
    "public_only": false,
    "policy": "付费/会员内容只保留页面公开可见部分，不绕过付费墙"
  },
  "crawl": {
    "success": true,
    "status_code": 200,
    "error": ""
  },
  "code_blocks": [],
  "images": [
    {
      "status": "ok",
      "url": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgcbeb6zt443dqmva3eiq.png",
      "bytes": 146088,
      "reused": true,
      "local_path": "attachments/d4026d1ae4e9/images/003.png",
      "src": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgcbeb6zt443dqmva3eiq.png",
      "alt": "Same hardware, same 38.5 seconds. The difference is whether the GPU is allowed to start the next group while the CPU is still checking the last one.",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2s7wr2cdo9dy31q4hccx.png",
      "bytes": 83174,
      "reused": true,
      "local_path": "attachments/d4026d1ae4e9/images/004.png",
      "src": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2s7wr2cdo9dy31q4hccx.png",
      "alt": "Three jobs (generate 2, environment 6, update 2 time units). Synchronous: done at 30. Overlapped with one gym worker: 22, the environment is the bottleneck. With the gym pool sized to T_env / T_gen = 3 workers: 14.",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xajup9nsmka6cgsmdvg.png",
      "bytes": 63330,
      "reused": true,
      "local_path": "attachments/d4026d1ae4e9/images/005.png",
      "src": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xajup9nsmka6cgsmdvg.png",
      "alt": "Utilization ρ = λ·E[S] / n for λ = 8 rollouts/s and E[S] = 6 s. Below 48 workers the queue grows without bound; about 63 gives the rule-of-thumb 30% headroom.",
      "downloaded": true
    },
    {
      "status": "ok",
      "url": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wroh3zydx04smwfq30b.png",
      "bytes": 59154,
      "reused": true,
      "local_path": "attachments/d4026d1ae4e9/images/006.png",
      "src": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wroh3zydx04smwfq30b.png",
      "alt": "With max_staleness = 1, a rollout is trained on only if the trainer moved at most one policy version while it was being generated and graded.",
      "downloaded": true
    }
  ],
  "audio": [],
  "videos": [],
  "links": [
    {
      "href": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgcbeb6zt443dqmva3eiq.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2s7wr2cdo9dy31q4hccx.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://www.g-ftech.com/blog/async-grpo-gym-rollout-scaling?utm_source=devto&utm_medium=syndication",
      "label": "original post",
      "type": "a"
    },
    {
      "href": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7xajup9nsmka6cgsmdvg.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wroh3zydx04smwfq30b.png",
      "label": "",
      "type": "a"
    },
    {
      "href": "https://www.g-ftech.com/solutions/private-training-environments?utm_source=devto&utm_medium=syndication",
      "label": "Private training environments",
      "type": "a"
    }
  ],
  "repair_version": 2,
  "repaired_at": "2026-10-14T10:48:56Z",
  "source_url": "https://dev.to/g_factor/asyncgrpo-eliminating-gpu-idle-bubbles-in-environment-heavy-rl-post-training-12c9",
  "papers": [],
  "actual_video_files": [],
  "body_chars": 13683,
  "expected_body_images": 4,
  "discovery_status": "ok",
  "delivery_status": "ok",
  "missing_reason": "",
  "raw_html": "attachments/d4026d1ae4e9/pages/source.html",
  "local_html": "attachments/d4026d1ae4e9/pages/article.html",
  "body_base_path": "attachments/d4026d1ae4e9/pages/",
  "body_markdown_path": "attachments/d4026d1ae4e9/pages/article.md"
}