<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Joinal Ahmed</title>
    <description>The latest articles on DEV Community by Joinal Ahmed (@joinalahmed).</description>
    <link>https://dev.to/joinalahmed</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F141980%2F5bbc24e8-c92f-4281-ae33-7ebaafae089b.jpg</url>
      <title>DEV Community: Joinal Ahmed</title>
      <link>https://dev.to/joinalahmed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joinalahmed"/>
    <language>en</language>
    <item>
      <title>Running Captum on Cloud TPUs with TorchTPU: a work log</title>
      <dc:creator>Joinal Ahmed</dc:creator>
      <pubDate>Sun, 01 Nov 2026 19:42:15 +0000</pubDate>
      <link>https://dev.to/joinalahmed/running-captum-on-cloud-tpus-with-torchtpu-a-work-log-4fpl</link>
      <guid>https://dev.to/joinalahmed/running-captum-on-cloud-tpus-with-torchtpu-a-work-log-4fpl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the log of a few days spent running Captum's attribution methods on Cloud TPUs through TorchTPU, in the order the work happened, with what I learned at each step. Unless stated otherwise, measurements are from a Cloud TPU v6e-1 in us-east5-b with torch 2.15.0, torch_tpu 0.2.0, Captum 0.9.0 and transformers 5.22.1. Code, scripts and raw findings are in &lt;a href="https://github.com/joinalahmed/captum-on-torchtpu" rel="noopener noreferrer"&gt;github.com/joinalahmed/captum-on-torchtpu&lt;/a&gt;; the last section lists them.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What I ended up with
&lt;/h2&gt;

&lt;p&gt;Before the log, the short version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Captum works on TorchTPU without modification. Every public &lt;code&gt;captum.attr&lt;/code&gt; method, including the &lt;code&gt;LLMAttribution&lt;/code&gt; wrappers, runs on &lt;code&gt;torch.device("tpu")&lt;/code&gt;. The 27 deterministic methods match CPU to about 1e-7 relative error; the 11 stochastic ones run and differ only through random sampling.&lt;/li&gt;
&lt;li&gt;The one setting that matters for correctness is matmul precision. At the default, TPU attributions differ from CPU by about 1e-3 (1e-2 for LayerGradCam). With &lt;code&gt;torch.set_float32_matmul_precision("highest")&lt;/code&gt; they agree to 1e-7 on small models and 1e-5 on Qwen3-0.6B, at no measurable cost for this workload.&lt;/li&gt;
&lt;li&gt;Attribution turned out to be a good lens on TorchTPU's execution modes. On Qwen3-0.6B, Layer Integrated Gradients with 64 steps takes 0.80 s in strict eager mode, 0.36 s with DeferAndFuse and 0.16 s under &lt;code&gt;torch.compile(backend="tpu")&lt;/code&gt;, against 4 to 17 s on CPU.&lt;/li&gt;
&lt;li&gt;Along the way I hit, and fixed or filed, a buffer leak for Python scalars, a missing &lt;code&gt;max_unpool2d&lt;/code&gt; lowering, a compiled-mode bug that read CPU view arguments past their buffer, a Captum bug that placed a CPU tensor in the compiled graph, eight unregistered ops and 11 stale deny-list entries. Five pull requests are open upstream; five more branches wait on a PR-count limit.&lt;/li&gt;
&lt;li&gt;On a four-chip v6e-4 everything held, every fix passed the tests upstream CI runs, and a device-side trace explained the eager-mode numbers: 363 XLA programs per attribution call and a 2% TensorCore duty cycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rest is how I got there.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Starting point: why attribution, and what already existed
&lt;/h2&gt;

&lt;p&gt;I wanted a workload that would exercise TorchTPU's execution modes, its numerical defaults and its profiler, and that was small enough to iterate on quickly. Attribution fit. Integrated Gradients interpolates between a baseline and the input over &lt;code&gt;n_steps&lt;/code&gt; points, runs a forward and backward pass at each point and integrates the gradients, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;repeated forward and backward passes with identical shapes, which a compiler can exploit;&lt;/li&gt;
&lt;li&gt;Python scalars (step sizes, scaling factors) flowing into device operations, which exercises eager dispatch and caching;&lt;/li&gt;
&lt;li&gt;forward hooks and &lt;code&gt;torch.autograd.grad&lt;/code&gt; on intermediate layers, which exercises the autograd integration;&lt;/li&gt;
&lt;li&gt;outputs that are differences of gradients, which make arithmetic error visible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first thing I did was check whether anyone had done this. A survey of Captum, SHAP, Inseq, TransformerLens, nnsight and pyvene (the report is in &lt;code&gt;reports/&lt;/code&gt;) found no TPU support statements, no TPU CI and no TPU-related issues or pull requests in any of them. Captum has around 355k downloads a month and runs CI on CPU and GPU only.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learned:&lt;/strong&gt; there was no prior art to lean on, which meant every result below needed its own CPU comparison rather than a reference I could cite.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Does anything run at all
&lt;/h2&gt;

&lt;p&gt;I started with a smoke test (&lt;code&gt;scripts/tpu_smoke.py&lt;/code&gt;) covering the autograd features attribution methods depend on, on deliberately small models so that any failure would come from the runtime and not from memory.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Primitive&lt;/th&gt;
&lt;th&gt;Result on TPU&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;loss.backward()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;torch.autograd.grad&lt;/code&gt; with respect to inputs&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;forward hooks&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;full backward hooks&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;double backward (&lt;code&gt;create_graph=True&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;custom &lt;code&gt;autograd.Function&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;torch.func.grad&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;torch.compile(backend="tpu")&lt;/code&gt; with backward on a view input&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Captum &lt;code&gt;IntegratedGradients&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Captum &lt;code&gt;LayerIntegratedGradients&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pass&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All twelve passed. &lt;code&gt;torch.accelerator.current_accelerator()&lt;/code&gt; returned &lt;code&gt;tpu&lt;/code&gt;, &lt;code&gt;is_available()&lt;/code&gt; true, &lt;code&gt;device_count()&lt;/code&gt; 1.&lt;/p&gt;

&lt;p&gt;Encouraged, I wrote &lt;code&gt;scripts/captum_tpu_matrix.py&lt;/code&gt; to run every &lt;code&gt;captum.attr&lt;/code&gt; class on an MLP and a small CNN, on CPU and on TPU, and compare the outputs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;27 methods matched CPU within 1e-7 to 1e-8: IntegratedGradients, Saliency, InputXGradient, DeepLift, GuidedBackprop, Deconvolution, GuidedGradCam, FeatureAblation, Occlusion, ShapleyValues, LRP and all the Layer* and Neuron* variants.&lt;/li&gt;
&lt;li&gt;11 ran but did not match bit for bit: GradientShap, ShapleyValueSampling, NoiseTunnel and the *Shap family. They sample random noise and the streams differ between devices, so that is expected.&lt;/li&gt;
&lt;li&gt;Nothing failed for a TPU reason. Lime and KernelShap failed on the first run because &lt;code&gt;sklearn&lt;/code&gt; was not installed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then a real model. &lt;code&gt;scripts/demo_captum_qwen_tpu.py&lt;/code&gt; loads Qwen3-0.6B with &lt;code&gt;AutoModelForCausalLM.from_pretrained(..., device_map="tpu")&lt;/code&gt;, which puts the weights on the device directly (2.9 s). &lt;code&gt;LayerIntegratedGradients&lt;/code&gt; over the embedding layer, &lt;code&gt;LLMAttribution(FeatureAblation)&lt;/code&gt; and &lt;code&gt;LLMGradientAttribution(LayerIntegratedGradients)&lt;/code&gt; all ran and the HTML heatmap rendered.&lt;/p&gt;

&lt;p&gt;For completeness I tried a few other libraries (&lt;code&gt;scripts/other_xai_libs_tpu.py&lt;/code&gt;). SHAP's DeepExplainer and GradientExplainer, pytorch-grad-cam's GradCAM and ScoreCAM, and nnsight's trace with &lt;code&gt;device_map="tpu"&lt;/code&gt; all worked. Inseq failed with &lt;code&gt;ValueError: Unknown device tpu&lt;/code&gt; from a hard-coded device list, a small upstream fix. TransformerLens failed on a transformers v5 incompatibility unrelated to TPUs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learned:&lt;/strong&gt; TorchTPU registers through the standard accelerator interface, so a library that is device-agnostic in the PyTorch sense needs no TPU-specific code. The failures I did see were allow-lists, not kernels.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The numbers were off by 1e-3, and why
&lt;/h2&gt;

&lt;p&gt;The matrix above was run with matmul precision set to "highest". My first pass had used the default, and the agreement was much worse. That turned into the single most useful finding of the project.&lt;/p&gt;

&lt;p&gt;By default TorchTPU runs float32 matmuls with bfloat16 inputs on the MXU. Model outputs tolerate this. Attributions do not, because they are differences of gradients and a difference magnifies the relative error of its terms.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Default precision&lt;/th&gt;
&lt;th&gt;&lt;code&gt;torch.set_float32_matmul_precision("highest")&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Saliency&lt;/td&gt;
&lt;td&gt;2.2e-3&lt;/td&gt;
&lt;td&gt;1.7e-7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InputXGradient&lt;/td&gt;
&lt;td&gt;1.7e-3&lt;/td&gt;
&lt;td&gt;7.0e-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IntegratedGradients (50 steps)&lt;/td&gt;
&lt;td&gt;2.1e-3&lt;/td&gt;
&lt;td&gt;4.4e-8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LayerIntegratedGradients&lt;/td&gt;
&lt;td&gt;3.1e-3&lt;/td&gt;
&lt;td&gt;1.0e-7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LayerGradCam&lt;/td&gt;
&lt;td&gt;1.0e-2&lt;/td&gt;
&lt;td&gt;2.2e-7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GradientShap&lt;/td&gt;
&lt;td&gt;3.6e-2&lt;/td&gt;
&lt;td&gt;2.5e-2 (stochastic)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;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%2Fmwdbhy799ncnrjzlaqly.png" class="article-body-image-wrapper"&gt;&lt;img 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%2Fmwdbhy799ncnrjzlaqly.png" alt="Max relative error vs CPU, default vs highest matmul precision" width="800" height="382"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Qwen3-0.6B in fp32 at "highest", &lt;code&gt;LayerIntegratedGradients&lt;/code&gt; on "The capital of France is" with target " Paris" came within 8.5e-6 of CPU and gave the same three top-attributed tokens (" France", " of", "The").&lt;/p&gt;

&lt;p&gt;I expected "highest" to cost something. It did not, measurably: on Qwen3 at 32 and 128 steps the warm eager times were 0.86 s and 1.17 s at "highest" versus 0.88 s and 1.08 s at "high". The profiles in section 5 explain why: at this model size the workload is bound by dispatch and host-device traffic, not the MXU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learned:&lt;/strong&gt; set &lt;code&gt;torch.set_float32_matmul_precision("highest")&lt;/code&gt; before any attribution work on TPU. It is the difference between 1e-3 and 1e-7, and for this workload it is free.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. A detour: my own batched IG, and a reference that had not converged
&lt;/h2&gt;

&lt;p&gt;Early on I assumed that Captum would be slow on TPU because it loops, and wrote a batched Integrated Gradients (&lt;code&gt;torchtpu_xai&lt;/code&gt;) that runs all steps as a single fixed-shape call. Two things came out of that.&lt;/p&gt;

&lt;p&gt;First, Captum already does this. With &lt;code&gt;internal_batch_size=None&lt;/code&gt;, &lt;code&gt;IntegratedGradients&lt;/code&gt; expands the input to &lt;code&gt;n_steps × batch&lt;/code&gt; and runs one forward and backward. My timings matched Captum's, and I dropped the separate implementation.&lt;/p&gt;

&lt;p&gt;Second, my implementation disagreed with Captum by 0.71 relative error on Qwen3, which took a while to understand. It was not a bug. Captum defaults to Gauss-Legendre quadrature and I had used a right Riemann sum; with &lt;code&gt;method="riemann_right"&lt;/code&gt; the two agreed to the printed digit. Checking that, though, showed the reference itself had not converged: Captum on CPU with 256 Gauss-Legendre steps reported a convergence delta of 27.3 against a total attribution of 44.4, and the total moved from -26.6 to -8.2 to +44.4 across 32, 128 and 256 steps. A raw-logit target with a pad-token baseline is simply not a well-conditioned IG setup for a language model.&lt;/p&gt;

&lt;p&gt;&lt;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%2Fymw3hzrdu4fusu67f0o1.png" class="article-body-image-wrapper"&gt;&lt;img 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%2Fymw3hzrdu4fusu67f0o1.png" alt="IG total attribution vs steps on Qwen3-0.6B" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learned:&lt;/strong&gt; before comparing two attribution implementations, check that the thing being compared has converged. &lt;code&gt;return_convergence_delta=True&lt;/code&gt; is cheap, and I put that advice into the docs I later contributed. Also: read the library before reimplementing it.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Execution modes, with attribution as the probe
&lt;/h2&gt;

&lt;p&gt;TorchTPU has three ways to run a program: strict eager, where each op is dispatched as called; DeferAndFuse (&lt;code&gt;TORCH_TPU_DEFER_AND_FUSE=1&lt;/code&gt;), where ops are deferred and fused into larger programs; and &lt;code&gt;torch.compile(backend="tpu")&lt;/code&gt;. Attribution exposes the differences more than a single forward pass does.&lt;/p&gt;

&lt;h3&gt;
  
  
  Qwen3-0.6B, 53-token prompt, Captum &lt;code&gt;LayerIntegratedGradients&lt;/code&gt;, precision "high"
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Steps&lt;/th&gt;
&lt;th&gt;Captum, CPU&lt;/th&gt;
&lt;th&gt;Captum, TPU strict eager&lt;/th&gt;
&lt;th&gt;Captum, TPU DeferAndFuse&lt;/th&gt;
&lt;th&gt;Batched IG, &lt;code&gt;torch.compile&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;4.48 s&lt;/td&gt;
&lt;td&gt;0.880 s&lt;/td&gt;
&lt;td&gt;0.238 s (first call 26 s)&lt;/td&gt;
&lt;td&gt;0.176 s (first call 68 to 112 s)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;128&lt;/td&gt;
&lt;td&gt;17.04 s&lt;/td&gt;
&lt;td&gt;1.078 s&lt;/td&gt;
&lt;td&gt;0.450 s (first call 29 s)&lt;/td&gt;
&lt;td&gt;out of memory (see section 6)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;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%2Fzynhr3rmx734jflhv8jd.png" class="article-body-image-wrapper"&gt;&lt;img 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%2Fzynhr3rmx734jflhv8jd.png" alt="LayerIntegratedGradients on Qwen3-0.6B across execution modes" width="799" height="311"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With DeferAndFuse, unmodified Captum ran 3.7 times faster than strict eager and 38 times faster than CPU at 128 steps, for a one-time fusion compile of 26 to 29 s.&lt;/p&gt;

&lt;h3&gt;
  
  
  The shipped example: Qwen3-0.6B, 42-token prompt, 64 steps, precision "highest"
&lt;/h3&gt;

&lt;p&gt;These are the numbers behind the example I later added to torch_tpu (&lt;code&gt;examples/explainability/captum_attribution.py&lt;/code&gt;).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Warm median&lt;/th&gt;
&lt;th&gt;First call&lt;/th&gt;
&lt;th&gt;Max rel. error vs CPU&lt;/th&gt;
&lt;th&gt;Peak HBM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;strict eager&lt;/td&gt;
&lt;td&gt;0.80 s&lt;/td&gt;
&lt;td&gt;5 s&lt;/td&gt;
&lt;td&gt;9.2e-05&lt;/td&gt;
&lt;td&gt;10.6 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeferAndFuse&lt;/td&gt;
&lt;td&gt;0.36 s&lt;/td&gt;
&lt;td&gt;43 s&lt;/td&gt;
&lt;td&gt;9.3e-05&lt;/td&gt;
&lt;td&gt;11.1 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;torch.compile(backend="tpu")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0.16 s&lt;/td&gt;
&lt;td&gt;130 s&lt;/td&gt;
&lt;td&gt;7.0e-05&lt;/td&gt;
&lt;td&gt;15.3 GiB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;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%2Fdsxb87hgc69kjsf7v5y5.png" class="article-body-image-wrapper"&gt;&lt;img 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%2Fdsxb87hgc69kjsf7v5y5.png" alt="The shipped example in three execution modes" width="799" height="224"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a small MLP the three modes take 0.065, 0.039 and 0.062 s with 2e-7 error each, so compiled mode only pays off once there is enough work per call to amortise the launch.&lt;/p&gt;

&lt;p&gt;Three things I had to learn the hard way here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First-call time is real. TorchTPU's tier-2 compilation cache is off for single-device programs, so each new process recompiles. The tier-3 cache (&lt;code&gt;TORCH_TPU_TIER3_COMPILATION_CACHE_ROOT=gs://...&lt;/code&gt;) persists compiled programs across processes.&lt;/li&gt;
&lt;li&gt;The compiled column did not work out of the box. Captum's &lt;code&gt;LayerIntegratedGradients&lt;/code&gt; around a compiled Hugging Face model failed with "expected tensor to be on tpu, got cpu". Section 7 covers the two causes, one in Captum and one in torch_tpu.&lt;/li&gt;
&lt;li&gt;At small sizes TPU time does not scale with steps. On a 5-token prompt it was about 0.55 s whether I ran 8, 32 or 64 steps. That is fixed overhead, and the differences between modes only show at tens of tokens and tens of steps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Learned:&lt;/strong&gt; for an unmodified Captum workload, DeferAndFuse is the setting that gives most of the speed-up with no code changes. Compile is faster still but needs patches and a long first call.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Profiling: where the time actually goes
&lt;/h2&gt;

&lt;p&gt;I profiled &lt;code&gt;LayerIntegratedGradients&lt;/code&gt; at 32 steps in each mode with &lt;code&gt;torch.profiler&lt;/code&gt;, &lt;code&gt;ProfilerActivity.TPU&lt;/code&gt; and &lt;code&gt;TpuProfilerConfig&lt;/code&gt; (&lt;code&gt;scripts/profile_ig_modes.py&lt;/code&gt;), and opened the traces in XProf.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Self CPU total&lt;/th&gt;
&lt;th&gt;&lt;code&gt;aten::_to_copy&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;_to_copy&lt;/code&gt; calls&lt;/th&gt;
&lt;th&gt;Average per copy&lt;/th&gt;
&lt;th&gt;Self TPU total&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;strict eager&lt;/td&gt;
&lt;td&gt;1.93 s&lt;/td&gt;
&lt;td&gt;1.35 s (70%)&lt;/td&gt;
&lt;td&gt;522&lt;/td&gt;
&lt;td&gt;2.6 ms&lt;/td&gt;
&lt;td&gt;5.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeferAndFuse&lt;/td&gt;
&lt;td&gt;0.78 s&lt;/td&gt;
&lt;td&gt;0.29 s&lt;/td&gt;
&lt;td&gt;522&lt;/td&gt;
&lt;td&gt;0.57 ms&lt;/td&gt;
&lt;td&gt;1.23 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;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%2F18p1utrzsxvq35kc4fem.png" class="article-body-image-wrapper"&gt;&lt;img 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%2F18p1utrzsxvq35kc4fem.png" alt="XProf: aten::_to_copy share of host time" width="799" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In strict eager mode 70% of host time is &lt;code&gt;aten::_to_copy&lt;/code&gt;: host-to-device and device-to-host copies that block the Python thread. Captum crosses the boundary constantly, for step sizes, for the scaling of inputs along the path, for the &lt;code&gt;cumsum&lt;/code&gt; offsets it uses to slice layer outputs. Each is a synchronous round trip in eager mode. DeferAndFuse does the same 522 copies but each costs a fifth as much, because a copy no longer waits for every preceding op individually.&lt;/p&gt;

&lt;h3&gt;
  
  
  The same picture from the device side
&lt;/h3&gt;

&lt;p&gt;Later, on the v6e-4, I captured a trace of a single warm 50-step &lt;code&gt;IntegratedGradients&lt;/code&gt; call on the small MLP with &lt;code&gt;torch_tpu._internal.profiler&lt;/code&gt; and read it with the XProf tools.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Profiled window&lt;/td&gt;
&lt;td&gt;23.3 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;XLA programs launched for one call&lt;/td&gt;
&lt;td&gt;363&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gaps between programs (median 57 us)&lt;/td&gt;
&lt;td&gt;22.3 ms, 95.8% of the window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TensorCore duty cycle&lt;/td&gt;
&lt;td&gt;2.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Device op time&lt;/td&gt;
&lt;td&gt;476 us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;X64SplitLow&lt;/code&gt; / &lt;code&gt;X64SplitHigh&lt;/code&gt; custom calls (302)&lt;/td&gt;
&lt;td&gt;231 us, 48.6% of op time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The MLP's four matmul fusions&lt;/td&gt;
&lt;td&gt;5.2 us, 0.02% of the window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MXU utilization&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Eager TorchTPU dispatches one XLA program per PyTorch op, so 50 interpolation points become 363 launches with a median 57 us gap between them, and the TensorCore sits idle 98% of the time. Half the device op time is &lt;code&gt;X64SplitLow&lt;/code&gt;/&lt;code&gt;X64SplitHigh&lt;/code&gt;, the custom calls that split 64-bit Python scalars into two 32-bit halves on arrival, which is the same scalar path as the leak in section 7.1. The matmuls that are the "model" take 5 microseconds. The trace and the full report are in &lt;code&gt;tpu_agent_project/results/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Memory
&lt;/h3&gt;

&lt;p&gt;Peak HBM is set by the logits tensor. A language-model attribution materialises &lt;code&gt;[steps, T, V]&lt;/code&gt; logits before selecting the last position and the target token. For Qwen3 at 128 steps and 53 tokens that is &lt;code&gt;[128, 53, 151936]&lt;/code&gt; in fp32, 3.84 GB, alongside the interpolation path and its gradients. That is what ran out of memory for the batched implementation at 128 steps and for everything at 256.&lt;/p&gt;

&lt;p&gt;Two user-side changes fix it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;model(input_ids, logits_to_keep=1)&lt;/code&gt; (transformers v5) keeps only the last position. With it, 128 steps in one batch took 0.857 s warm on TPU at 23.9 GB peak, against 11.2 s on CPU.&lt;/li&gt;
&lt;li&gt;Captum's &lt;code&gt;internal_batch_size&lt;/code&gt; chunks the steps. 128 steps with &lt;code&gt;internal_batch_size=64&lt;/code&gt; took 1.42 s at 10.6 GiB peak instead of 23.9 GB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;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%2Fmfry5qdmz8dxpgk2s3nw.png" class="article-body-image-wrapper"&gt;&lt;img 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%2Fmfry5qdmz8dxpgk2s3nw.png" alt="Peak HBM by step count and internal_batch_size" width="800" height="328"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While measuring this I found the &lt;code&gt;torch.accelerator&lt;/code&gt; memory APIs (&lt;code&gt;memory_stats&lt;/code&gt;, &lt;code&gt;max_memory_allocated&lt;/code&gt;, &lt;code&gt;empty_cache&lt;/code&gt; and friends) undocumented for TorchTPU, and wrote the section that became the &lt;code&gt;docs-memory-apis&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learned:&lt;/strong&gt; eager attribution on TorchTPU is launch-bound and copy-bound, from both the host and the device side, and the fix is a different execution mode, not a faster chip. Memory is a logits problem; &lt;code&gt;logits_to_keep&lt;/code&gt; and &lt;code&gt;internal_batch_size&lt;/code&gt; are the two knobs.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. The bugs, in the order I hit them
&lt;/h2&gt;

&lt;p&gt;Every item here started as something Captum does that a training loop does not.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.1 Device buffers for Python scalars were never released (issue &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6036" rel="noopener noreferrer"&gt;#6036&lt;/a&gt;, PR &lt;a href="https://github.com/google-pytorch/torch_tpu/pull/6102" rel="noopener noreferrer"&gt;#6102&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;IG scales inputs by a different Python float at every step. An open issue reported that eager-mode device buffers for distinct Python scalars were never evicted, and I wanted to know whether Captum triggered it. &lt;code&gt;scripts/scalar_leak_check.py&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Loop&lt;/th&gt;
&lt;th&gt;Live allocations added after 5000 iterations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x.add_(0.5)&lt;/code&gt; (same scalar each time)&lt;/td&gt;
&lt;td&gt;+2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;x.add_(i / 1024)&lt;/code&gt; (new scalar each time)&lt;/td&gt;
&lt;td&gt;+4999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;after &lt;code&gt;empty_cache()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;none released&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It did. Twenty attribution calls with the same &lt;code&gt;n_steps&lt;/code&gt; added nothing, but sweeping &lt;code&gt;n_steps&lt;/code&gt; from 10 to 109 added 6361 allocations, because each &lt;code&gt;n_steps&lt;/code&gt; yields a new set of step-size floats.&lt;/p&gt;

&lt;p&gt;&lt;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%2F5cyeisbuff2cfjqb84qr.png" class="article-body-image-wrapper"&gt;&lt;img 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%2F5cyeisbuff2cfjqb84qr.png" alt="Scalar buffer leak before and after the fix" width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix is a bounded per-thread cache in &lt;code&gt;MakeBuffer&lt;/code&gt; in &lt;code&gt;csrc/eager/op_dispatcher.cc&lt;/code&gt;, with a regression test. With the patch, the 5000-scalar loop adds 903 allocations (the 1024-entry cache clears four times) and the Captum sweep adds 280. On current main the strict-eager case no longer leaks after a transfer refactor, but DeferAndFuse still does (+5001), so the fix stays relevant.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.2 &lt;code&gt;aten::max_unpool2d&lt;/code&gt; was not implemented (issue &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6103" rel="noopener noreferrer"&gt;#6103&lt;/a&gt;, PR &lt;a href="https://github.com/google-pytorch/torch_tpu/pull/6105" rel="noopener noreferrer"&gt;#6105&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;To get beyond my own two toy models I ported Captum's 78 attribution test configurations into a device-generic suite that runs them on CPU and on &lt;code&gt;CAPTUM_TEST_DEVICE=tpu&lt;/code&gt; and compares. Of 266 generated tests, 262 passed and 4 failed, in 5 min 25 s.&lt;/p&gt;

&lt;p&gt;One failure was a tolerance miss at a near-zero attribution (+6.1e-5 vs -6.1e-5), precision noise. The other three were one gap: DeepLift's MaxPool rule calls &lt;code&gt;F.max_unpool2d&lt;/code&gt;, TorchTPU had no lowering, and every DeepLift variant on a CNN failed with &lt;code&gt;RuntimeError: operator 'aten::max_unpool2d' is not implemented for TPU&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I filed the issue and implemented &lt;code&gt;max_unpool2d&lt;/code&gt; and &lt;code&gt;max_unpool3d&lt;/code&gt; as a StableHLO batched scatter. 25 unit tests and 4 error-message tests pass on v6e, and the three DeepLift configurations now pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.3 Compiled partitions read CPU view arguments past their buffer (PR &lt;a href="https://github.com/google-pytorch/torch_tpu/pull/6106" rel="noopener noreferrer"&gt;#6106&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;Chasing the "expected tensor to be on tpu, got cpu" failure under &lt;code&gt;torch.compile&lt;/code&gt;, I reduced it and found a different bug on the way: &lt;code&gt;x * cpu_tensor[1].to(tpu)&lt;/code&gt; under &lt;code&gt;torch.compile(backend="tpu")&lt;/code&gt; returned zero. A StableHLO dump showed the TPU partition compiled against the view's strides and storage offset, while at run time the argument is copied to the device contiguous. The compiled program read past the buffer it was given.&lt;/p&gt;

&lt;p&gt;The fix in &lt;code&gt;split_compiler.py&lt;/code&gt; compiles TPU partitions against the contiguous layout the argument is actually copied with. Test included, verified on v6e, CI green.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.4 Captum placed a CPU tensor in the compiled graph (Captum PR &lt;a href="https://github.com/meta-pytorch/captum/pull/1952" rel="noopener noreferrer"&gt;#1952&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;The other half of the compile failure is in Captum. &lt;code&gt;LayerIntegratedGradients&lt;/code&gt; computes output offsets with &lt;code&gt;torch.cumsum&lt;/code&gt; on a CPU tensor and slices layer outputs with it; under &lt;code&gt;torch.compile&lt;/code&gt; that CPU tensor becomes a graph input on the wrong device. Replacing it with Python integers fixes it, and 12 &lt;code&gt;LayerIntegratedGradients&lt;/code&gt; tests pass on TPU.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://github.com/meta-pytorch/captum/pull/1952" rel="noopener noreferrer"&gt;#1952&lt;/a&gt; and &lt;a href="https://github.com/google-pytorch/torch_tpu/pull/6106" rel="noopener noreferrer"&gt;#6106&lt;/a&gt; applied, the compiled row in section 5 runs: 0.16 s warm, 7.0e-05 error.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.5 Eight unregistered ops (branch &lt;code&gt;missing-ops-batch&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;scripts/probe_unregistered_ops.py&lt;/code&gt; calls every ATen op TorchTPU does not register with a plausible input. From the list I implemented eight with tests: &lt;code&gt;smooth_l1_loss&lt;/code&gt; and &lt;code&gt;huber_loss&lt;/code&gt; with backward, &lt;code&gt;logaddexp&lt;/code&gt;, &lt;code&gt;logaddexp2&lt;/code&gt;, &lt;code&gt;nansum&lt;/code&gt;, &lt;code&gt;std_mean&lt;/code&gt;, &lt;code&gt;trace&lt;/code&gt; and &lt;code&gt;addbmm&lt;/code&gt;. 53 tests pass on v6e against a wheel built from the branch; 23 files, 1733 lines.&lt;/p&gt;

&lt;p&gt;The thing I did not know going in: TorchTPU does not accept new composite kernels. If a &lt;code&gt;TT_KERNEL&lt;/code&gt; calls another ATen op, &lt;code&gt;OpNameStack&lt;/code&gt; aborts at run time with &lt;code&gt;CompositeOpCheck: Op trace is decomposed into as_strided ... implement the trace op by lowering it to SHLO instead&lt;/code&gt;, and the permitted-composite list is capped at 150 entries with a comment not to add more. &lt;code&gt;nansum&lt;/code&gt;, &lt;code&gt;trace&lt;/code&gt;, &lt;code&gt;std_mean&lt;/code&gt; and &lt;code&gt;addbmm&lt;/code&gt; compiled fine as composites and failed at run time, and I rewrote each as a direct StableHLO lowering: a diagonal mask and reduction for &lt;code&gt;trace&lt;/code&gt;; &lt;code&gt;Select(IsNan(x), 0, x)&lt;/code&gt; then a sum for &lt;code&gt;nansum&lt;/code&gt;; &lt;code&gt;Sqrt(Var)&lt;/code&gt; and &lt;code&gt;Mean&lt;/code&gt; on the shared reduction code for &lt;code&gt;std_mean&lt;/code&gt;; a batched matmul then a sum over the batch axis for &lt;code&gt;addbmm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Smaller things for the next contributor: &lt;code&gt;IgnoreInCacheKey&lt;/code&gt; rejects &lt;code&gt;PromotedScalar&lt;/code&gt; because scalars are already excluded from cache keys; &lt;code&gt;UnaryOpOptions&lt;/code&gt; has no default constructor, so &lt;code&gt;.op_param_cache_keys&lt;/code&gt; must be set; TPU matmul tests need &lt;code&gt;set_float32_matmul_precision("highest")&lt;/code&gt; to match CPU.&lt;/p&gt;

&lt;p&gt;Still unregistered: &lt;code&gt;triu_indices&lt;/code&gt;, &lt;code&gt;median&lt;/code&gt;, &lt;code&gt;nanmedian&lt;/code&gt;, &lt;code&gt;mode&lt;/code&gt;, &lt;code&gt;kthvalue&lt;/code&gt;, &lt;code&gt;frexp&lt;/code&gt;, &lt;code&gt;renorm&lt;/code&gt;, and the &lt;code&gt;.out&lt;/code&gt; variants of about a dozen more.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.6 The bounded-dynamism deny list (branch &lt;code&gt;dynamism-denylist-triage&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;TorchTPU's bounded-dynamism tests carry a deny list of ops marked "untriaged: invalid result". I removed the skips for 12 of them and ran the eager and compiled dynamic targets on v6e. On a single-chip host this needs &lt;code&gt;--local_test_jobs=1&lt;/code&gt;, otherwise parallel shards fight over &lt;code&gt;/dev/vfio/0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Eleven pass in both modes: &lt;code&gt;eq&lt;/code&gt;, &lt;code&gt;maximum&lt;/code&gt;, &lt;code&gt;ne&lt;/code&gt;, &lt;code&gt;neg&lt;/code&gt;, &lt;code&gt;pow&lt;/code&gt;, &lt;code&gt;remainder&lt;/code&gt;, &lt;code&gt;round&lt;/code&gt;, &lt;code&gt;sign&lt;/code&gt;, &lt;code&gt;sinh&lt;/code&gt;, &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;trunc&lt;/code&gt;. Their entries go. One is a real defect: &lt;code&gt;clamp.Tensor&lt;/code&gt; (tensor-valued min and max) mismatches 80% of elements whenever any dimension is bounded, while the scalar variants pass. Its entry now carries the diagnosis and reproduction conditions.&lt;/p&gt;

&lt;h3&gt;
  
  
  7.7 Smaller items
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Loader error message (branch &lt;code&gt;loader-import-error-context&lt;/code&gt;): a wheel built from source only imports against the torch pinned in the repo lockfile, and the failure was a bare &lt;code&gt;undefined symbol c10::report_overflow&lt;/code&gt;. The loader now names the glue library it chose and the PyTorch version it found. Test included.&lt;/li&gt;
&lt;li&gt;Memory API docs (branch &lt;code&gt;docs-memory-apis&lt;/code&gt;, addresses &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/4413" rel="noopener noreferrer"&gt;#4413&lt;/a&gt;): the &lt;code&gt;torch.accelerator&lt;/code&gt; memory APIs documented with values measured on v6e.&lt;/li&gt;
&lt;li&gt;Triage on &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/4408" rel="noopener noreferrer"&gt;#4408&lt;/a&gt;: the reported NaN in compiled SDPA backward does not reproduce on 0.2.0 with torch 2.15 (Qwen3; compiled vs CPU 1.85e-5).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/google-pytorch/torch_tpu/issues/4409" rel="noopener noreferrer"&gt;#4409&lt;/a&gt; (compile with backward on a view input) did not reproduce in my smoke test. Noted, not yet verified numerically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Learned:&lt;/strong&gt; the bugs clustered around two things Captum does unusually often, Python scalars into device ops and CPU-side bookkeeping tensors next to device tensors. Neither shows up in a training loop. Also: read a project's contributor constraints (the composite-op cap, the three-open-PR limit for outside contributors) before writing the code, not after.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Four chips: what changes and what does not (2026-11-01)
&lt;/h2&gt;

&lt;p&gt;Everything so far ran on one v6e chip. I queued a v6e-4 (four chips on one host) to answer two questions: do the results hold on a multi-chip host, and does splitting an attribution across chips help. Capacity took several hours to arrive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Process model.&lt;/strong&gt; The first thing I learned is that libtpu is exclusive per process. A second process touching the TPU while one is running fails with "The TPU is already in use by process with pid N". Multi-chip work on TorchTPU is one worker process per chip, launched with torchrun after exporting the slice topology:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; torch.tpu.distributed | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^/export /'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
torchrun &lt;span class="nt"&gt;--nproc_per_node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$WORLD_SIZE&lt;/span&gt; captum_multichip_ig.py &lt;span class="nt"&gt;--steps&lt;/span&gt; 256
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside a worker, &lt;code&gt;torch.accelerator.device_count()&lt;/code&gt; is 1 and &lt;code&gt;torch.device("tpu")&lt;/code&gt; is that worker's chip. The &lt;code&gt;tpu_dist&lt;/code&gt; backend provides the usual &lt;code&gt;torch.distributed&lt;/code&gt; collectives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Correctness.&lt;/strong&gt; The 38-method matrix on chip 0 gave the same result as the v6e-1: 27 match CPU, 11 stochastic run. I then sharded Integrated Gradients across the four chips, each rank evaluating a quarter of the Riemann grid in one batched forward and backward, with an &lt;code&gt;all_reduce&lt;/code&gt; assembling the attribution. It reproduces Captum's single-chip &lt;code&gt;LayerIntegratedGradients&lt;/code&gt; to 5.6e-8 when both sides use the same batch size and 1.2e-4 when they do not (reduction order, as in section 3).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; Qwen3-0.6B, fp32, 53 tokens, strict eager, released wheel:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;n_steps&lt;/th&gt;
&lt;th&gt;4 chips, sharded&lt;/th&gt;
&lt;th&gt;1 chip, Captum&lt;/th&gt;
&lt;th&gt;speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;0.355 s&lt;/td&gt;
&lt;td&gt;0.219 s (all 64 in one batch)&lt;/td&gt;
&lt;td&gt;0.62x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;0.327 s&lt;/td&gt;
&lt;td&gt;0.547 s (&lt;code&gt;internal_batch_size=64&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;1.67x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;0.357 s&lt;/td&gt;
&lt;td&gt;out of memory in a single batch&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 64 steps one chip finishes in a fifth of a second and splitting four ways costs more in dispatch and all-reduce than it saves. At 256 steps one chip cannot hold the batch, Captum has to chunk it into four sequential pieces, and four chips run those in parallel. 1.67x rather than 4x because per-chunk fixed costs and the all-reduce are not amortised at this size.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fixes on four chips.&lt;/strong&gt; I rebuilt each branch from source on the host and ran the bazel targets upstream CI uses (&lt;code&gt;_tpu-v6e&lt;/code&gt; variant) plus the matching pytest files against the installed wheel. The bounded scalar cache test passes and a four-worker torchrun of the leak check gives every rank the same counts as the single chip; the &lt;code&gt;max_unpool&lt;/code&gt;, eight-op and eleven-op dynamic tests pass; the split-compile test passes; the loader test passes; the memory APIs return per-process values; the example runs in all three modes at 2.0e-7. Logs are under &lt;code&gt;results/v6e-4/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong.&lt;/strong&gt; Partway through, the host-side TPU session manager on the first v6e-4 started refusing every process (&lt;code&gt;tpunetd START_SESSION failed&lt;/code&gt;). A guest reboot did not clear it, nor did restarting the runtime service. Queued-resource nodes cannot be stopped and started, so the only remedy was to delete the node and queue another with the same persistent build disk attached. The bazel cache on that disk took the replacement from bare image to its first branch test in under 20 minutes, which is the strongest argument I have for keeping build state on a separate disk.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not covered:&lt;/strong&gt; the v5e-8 that upstream CI runs on, and &lt;code&gt;tests/distributed/&lt;/code&gt;. v5e capacity was unavailable all day and a v5litepod-8 exceeds the project's 4-chip per-zone serving quota.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learned:&lt;/strong&gt; multi-chip attribution is worth it only when a single chip runs out of memory. Build from source on the host, with the bazel cache on a detachable disk. Expect a TPU host to need replacing at some point and plan the setup so that costs 20 minutes rather than a day.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. What went upstream
&lt;/h2&gt;

&lt;p&gt;To torch_tpu, on the &lt;code&gt;explainability-example&lt;/code&gt; branch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;examples/explainability/captum_attribution.py&lt;/code&gt;: one script with flags for the model (&lt;code&gt;tiny&lt;/code&gt; or any Hugging Face causal LM), steps, runs, precision, &lt;code&gt;--compile&lt;/code&gt;, &lt;code&gt;--internal-batch-size&lt;/code&gt; and &lt;code&gt;--profile-dir&lt;/code&gt;. It reports CPU reference time, first-call time, warm median, max relative error against CPU and peak HBM.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docs/source/case_studies_explainability.py&lt;/code&gt; and its Markdown export: a seven-section case study covering running Captum on a TPU, setting matmul precision, comparing execution modes, reading the XProf trace, bounding memory, checking convergence and running the example. Linked from the case-study index and the examples README; &lt;code&gt;mkdocs build --strict&lt;/code&gt; passes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To Captum, &lt;code&gt;tests/attr/test_accelerator.py&lt;/code&gt; (&lt;a href="https://github.com/meta-pytorch/captum/pull/1951" rel="noopener noreferrer"&gt;meta-pytorch/captum#1951&lt;/a&gt;), which re-runs Captum's 78 attribution configurations on CPU and on any accelerator and compares them, so TPU compatibility is covered by tests rather than by this post.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. If you want to try this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Strict eager and DeferAndFuse work on torch_tpu 0.2.0 as released. Compiled mode needs two unmerged patches (Captum &lt;a href="https://github.com/meta-pytorch/captum/pull/1952" rel="noopener noreferrer"&gt;#1952&lt;/a&gt;, torch_tpu &lt;a href="https://github.com/google-pytorch/torch_tpu/pull/6106" rel="noopener noreferrer"&gt;#6106&lt;/a&gt;). Until they land, DeferAndFuse is the no-changes option.&lt;/li&gt;
&lt;li&gt;Set matmul precision to "highest" and check the convergence delta.&lt;/li&gt;
&lt;li&gt;Five branches (&lt;code&gt;missing-ops-batch&lt;/code&gt;, &lt;code&gt;explainability-example&lt;/code&gt;, &lt;code&gt;dynamism-denylist-triage&lt;/code&gt;, &lt;code&gt;docs-memory-apis&lt;/code&gt;, &lt;code&gt;loader-import-error-context&lt;/code&gt;) are pushed to the fork without upstream PRs yet. The repository allows users without write access three open PRs and the first three fill the quota. Each is tracked by an issue (&lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6111" rel="noopener noreferrer"&gt;#6111&lt;/a&gt; to &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6114" rel="noopener noreferrer"&gt;#6114&lt;/a&gt;, and a comment on &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/4413" rel="noopener noreferrer"&gt;#4413&lt;/a&gt;) so review can start before the PR exists.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  11. Where everything lives
&lt;/h2&gt;

&lt;p&gt;&lt;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%2F4b7hfh5ucr5s5y7p0vf9.png" class="article-body-image-wrapper"&gt;&lt;img 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%2F4b7hfh5ucr5s5y7p0vf9.png" alt="Contributions map" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Open upstream&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/google-pytorch/torch_tpu/pull/6102" rel="noopener noreferrer"&gt;google-pytorch/torch_tpu#6102&lt;/a&gt;: bounded scalar buffer cache (fixes &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6036" rel="noopener noreferrer"&gt;#6036&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6103" rel="noopener noreferrer"&gt;google-pytorch/torch_tpu#6103&lt;/a&gt;: issue, &lt;code&gt;aten::max_unpool2d&lt;/code&gt; missing&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/google-pytorch/torch_tpu/pull/6105" rel="noopener noreferrer"&gt;google-pytorch/torch_tpu#6105&lt;/a&gt;: &lt;code&gt;max_unpool2d&lt;/code&gt; / &lt;code&gt;max_unpool3d&lt;/code&gt; lowerings&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/google-pytorch/torch_tpu/pull/6106" rel="noopener noreferrer"&gt;google-pytorch/torch_tpu#6106&lt;/a&gt;: fix compiled TPU partitions reading CPU view arguments past their buffer&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/meta-pytorch/captum/pull/1951" rel="noopener noreferrer"&gt;meta-pytorch/captum#1951&lt;/a&gt;: device-generic accelerator comparison tests&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/meta-pytorch/captum/pull/1952" rel="noopener noreferrer"&gt;meta-pytorch/captum#1952&lt;/a&gt;: Python ints for LayerIntegratedGradients output offsets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pushed, PR pending&lt;/strong&gt; (fork &lt;a href="https://github.com/joinalahmed/torch_tpu" rel="noopener noreferrer"&gt;&lt;code&gt;joinalahmed/torch_tpu&lt;/code&gt;&lt;/a&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/joinalahmed/torch_tpu/tree/missing-ops-batch" rel="noopener noreferrer"&gt;&lt;code&gt;missing-ops-batch&lt;/code&gt;&lt;/a&gt;: &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6111" rel="noopener noreferrer"&gt;google-pytorch/torch_tpu#6111&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/joinalahmed/torch_tpu/tree/explainability-example" rel="noopener noreferrer"&gt;&lt;code&gt;explainability-example&lt;/code&gt;&lt;/a&gt;: &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6114" rel="noopener noreferrer"&gt;google-pytorch/torch_tpu#6114&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/joinalahmed/torch_tpu/tree/dynamism-denylist-triage" rel="noopener noreferrer"&gt;&lt;code&gt;dynamism-denylist-triage&lt;/code&gt;&lt;/a&gt;: &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6112" rel="noopener noreferrer"&gt;google-pytorch/torch_tpu#6112&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/joinalahmed/torch_tpu/tree/docs-memory-apis" rel="noopener noreferrer"&gt;&lt;code&gt;docs-memory-apis&lt;/code&gt;&lt;/a&gt;: comment on &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/4413" rel="noopener noreferrer"&gt;google-pytorch/torch_tpu#4413&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/joinalahmed/torch_tpu/tree/loader-import-error-context" rel="noopener noreferrer"&gt;&lt;code&gt;loader-import-error-context&lt;/code&gt;&lt;/a&gt;: &lt;a href="https://github.com/google-pytorch/torch_tpu/issues/6113" rel="noopener noreferrer"&gt;google-pytorch/torch_tpu#6113&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;In this repository&lt;/strong&gt; (&lt;a href="https://github.com/joinalahmed/captum-on-torchtpu" rel="noopener noreferrer"&gt;joinalahmed/captum-on-torchtpu&lt;/a&gt;)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;images/&lt;/code&gt;: the figures in this post, generated by &lt;a href="https://github.com/joinalahmed/captum-on-torchtpu/blob/main/scripts/make_blog_images.py" rel="noopener noreferrer"&gt;&lt;code&gt;scripts/make_blog_images.py&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FINDINGS.md&lt;/code&gt;: the measurements, in the order they were taken&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scripts/&lt;/code&gt;: every benchmark, probe and reproduction above, plus &lt;code&gt;tpu_vm.sh&lt;/code&gt; for creating, setting up and deleting the VM&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;captum_on_torchtpu.ipynb&lt;/code&gt;: notebook version of sections 2 to 6&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reports/Explainability tools on TPUs.md&lt;/code&gt;: the prior-art survey&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;results/v6e-4/&lt;/code&gt;: the four-chip matrix, the sharded IG numbers and every branch's multi-chip test log&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tpu_agent_project/&lt;/code&gt;: the same workload dispatched through Google's TPU agent suite, with the XProf report quoted in section 6&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost.&lt;/strong&gt; The single-chip work ran on a v6e-1 created and deleted per work session; the four-chip work on one v6e-4 from a queued resource after several hours of waiting. VMs and disks bill while they exist; everything was deleted when the work finished.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written with Claude Code as the pair programmer. Commits in torch_tpu carry an &lt;code&gt;Assisted-by&lt;/code&gt; trailer, as that project's contributing guide requests; AI is not listed as a co-author there.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pytorch</category>
      <category>tpu</category>
      <category>machinelearning</category>
      <category>performance</category>
    </item>
    <item>
      <title>Operationalising MLOps on AWS</title>
      <dc:creator>Joinal Ahmed</dc:creator>
      <pubDate>Tue, 16 Nov 2021 07:57:44 +0000</pubDate>
      <link>https://dev.to/aws-builders/mlops-on-aws-236h</link>
      <guid>https://dev.to/aws-builders/mlops-on-aws-236h</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Purpose :&lt;/strong&gt;&lt;br&gt;
Machine learning development can be a complex and expensive process and there are barriers to adoption at every step of the machine learning workflow from data aggregation to preprocessing, which is time consuming and compute intensive, to choosing the right algorithm, which is often done by trial and error, to long running training cycles, which leads to increased costs. In this blog we will discuss recommendations around provisioning a cost effective, manageable, secure, scalable, high performance, efficient, highly available, fault tolerant and recoverable  MLOps architecture that an organization can adopt to solve pain points around Collaboration, Scaling, Reproducibility, Testing, Monitoring, Security and deployment strategies around ML projects lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Background :&lt;/strong&gt;&lt;br&gt;
Data science and analytics teams are often seen operating between increasing business expectations and abstract environments developing into complex solutions. This makes it challenging to transform data into solid answers for stakeholders consistently. In such a situation, &lt;strong&gt;&lt;em&gt;How can teams overcome complexity and live up to the expectations placed on them and deliver business impacts?&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
MLOps provides some answers, There is no one size fits all when it comes to implementing an MLOps solution on Amazon Web Services (AWS). Like any other technical solution, MLOps should be implemented to meet the project requirements. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Machine Learning Life Cycle&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
One way to categorize a machine learning platform's capabilities is through the machine learning life cycle stages. The typical ML life cycle can be described in the following nine steps:&lt;/p&gt;

&lt;p&gt;&lt;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.amazonaws.com%2Fuploads%2Farticles%2Fjizc2sbqbgzuqj6q0adi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjizc2sbqbgzuqj6q0adi.png" alt="ML Life Cycle" width="715" height="431"&gt;&lt;/a&gt;&lt;br&gt;
Starting by &lt;em&gt;sourcing data&lt;/em&gt;, a data scientist will &lt;em&gt;explore and analyze&lt;/em&gt; it. The raw data is &lt;em&gt;transformed&lt;/em&gt; into valuable features, typically involving &lt;em&gt;scheduling and automation&lt;/em&gt; to do this regularly. The resultant features are &lt;em&gt;stored and managed&lt;/em&gt;, available for the various models and other data scientists to use. As part of the exploration, the data scientist will also &lt;em&gt;build, train, and evaluate&lt;/em&gt; multiple models. Promising models are &lt;em&gt;stored and deployed&lt;/em&gt; into production. The production models are then &lt;em&gt;served and monitored&lt;/em&gt; for a while. Typically, there are numerous competing models in production, and choosing between them or evaluating them is done via &lt;em&gt;experimentation&lt;/em&gt;. With the learnings of the production models, the data scientist iterates on new features and models.&lt;/p&gt;

&lt;p&gt;As an organization, Looking towards leveraging ML in various aspects in their business, from gathering insights on the products (based on reviews, tweets etc), build user experiences (recommendation, personalization) or create impactful solutions around healthcare, ecommerce etc, there are some major concerns around CI/CD, ML lifecycle, collaboration, monitoring, experiments tracking, testing, security and infrastructure that a MLOps platform should address and empower data scientists to create ML solutions that drive direct business impact. &lt;br&gt;
These solutions can range from simple analyses to production ML systems that serve millions of customers. The MLOps Platform aims to provide these users with a unified set of tools to develop and confidently deploy their ML solutions rapidly.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Key Components of an MLOps Solution&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
    This section briefly discusses the key components for implementing an MLOps solution:&lt;br&gt;
    &lt;/p&gt;
&lt;ol&gt;

        &lt;li&gt;A &lt;strong&gt;version control system&lt;/strong&gt; to store, track, and version changes to ML code.&lt;/li&gt;

        &lt;li&gt;A &lt;strong&gt;version control system&lt;/strong&gt; to track and version changes to your datasets.&lt;/li&gt;

        &lt;li&gt;A &lt;strong&gt;network layer&lt;/strong&gt; that implements the necessary network resources to ensure the MLOps solution is secured.&lt;/li&gt;

        &lt;li&gt;An &lt;strong&gt;ML-based workload&lt;/strong&gt; to execute machine learning tasks. AWS  has a three-layered ML stack to choose from based on skill level.&lt;/li&gt;

        &lt;ol&gt;

            &lt;li&gt; &lt;em&gt;AI services:&lt;/em&gt; They are a fully managed set of services that enables users to quickly add ML capabilities to workloads using API calls. E.g: &lt;a href="https://aws.amazon.com/rekognition/" rel="noopener noreferrer"&gt; Amazon Rekognition&lt;/a&gt; and&lt;a href="https://aws.amazon.com/comprehend/" rel="noopener noreferrer"&gt; Amazon Comprehend&lt;/a&gt;.&lt;/li&gt;

            &lt;li&gt; &lt;em&gt;ML services:&lt;/em&gt; AWS provides managed services and resources (&lt;a href="https://aws.amazon.com/sagemaker/" rel="noopener noreferrer"&gt;Amazon SageMaker suite&lt;/a&gt;, for example) to enable users to label data and build, train, deploy, and operate ML models.&lt;/li&gt;

            &lt;li&gt; &lt;em&gt;ML frameworks and infrastructure:&lt;/em&gt; This is for  expert ML practitioners using open-source frameworks like TensorFlow, PyTorch, and Apache MXNet; Deep Learning AMI for Amazon EC2 instances; and Deep Learning Containers to implement own tools and workflows to build, train, and deploy the ML models.&lt;/li&gt;

        &lt;/ol&gt;

        &lt;li&gt; Use &lt;strong&gt;infrastructure as code (IaC)&lt;/strong&gt; to automate the provisioning and configuration of your cloud-based ML workloads and other IT infrastructure resources.&lt;/li&gt;

        &lt;li&gt;An &lt;strong&gt;ML (training/retraining) pipeline&lt;/strong&gt; to automate the steps required to train/retrain and deploy your ML models.&lt;/li&gt;

        &lt;li&gt;An &lt;strong&gt;orchestration tool&lt;/strong&gt; to orchestrate and execute your automated ML workflow steps.&lt;/li&gt;

        &lt;li&gt;A &lt;strong&gt;model monitoring solution&lt;/strong&gt; to monitor production models’ performance to protect against both model and data drift. You can also use the performance metrics as feedback to help improve the models’ future development and training. &lt;/li&gt;

        &lt;li&gt;A &lt;strong&gt;model governance framework&lt;/strong&gt; to make it easier to track, compare, and reproduce your ML experiments and secure your ML models.&lt;/li&gt;

        &lt;li&gt;A &lt;strong&gt;data platform&lt;/strong&gt; like&lt;a href="https://aws.amazon.com/s3/" rel="noopener noreferrer"&gt; Amazon Simple Storage Service&lt;/a&gt; (Amazon S3) to store your datasets.&lt;/li&gt;

        &lt;/ol&gt;


&lt;p&gt;&lt;strong&gt;&lt;em&gt;Setting up a MLOps Pipeline&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Data science and analytics teams are often squeezed between increasing business expectations and sandbox environments evolving into complex solutions. This makes it challenging to transform data into solid answers for stakeholders consistently. &lt;br&gt;
The ML-based workloads should support  the reproducibility in any machine learning pipeline, which is central to any MLOps solution. The ML-based workload implementation choice can directly impact the design and implementation of any MLOps solution.&lt;br&gt;
If the ML capabilities required by your use cases can be implemented using the &lt;em&gt;AI services&lt;/em&gt;, then an MLOps solution is not required. For instance the business use case to track the sentiment of users from their social media content (tweets, facebook posts) which leverages AWS AI services like Comprehend and Translate to extract insights can be well supported with a minimal solution where an listener running on a Amazon EC2 instance ingesting tweets / posts and delivering them via Kinesis Data Firehose and storing the raw content in S3 bucket. Amazon S3 invokes an&lt;a href="https://aws.amazon.com/lambda/" rel="noopener noreferrer"&gt; AWS Lambda&lt;/a&gt; function to analyze the raw tweets using&lt;a href="https://aws.amazon.com/translate/" rel="noopener noreferrer"&gt; Amazon Translate&lt;/a&gt; to translate non-English tweets into English, and&lt;a href="https://aws.amazon.com/comprehend/" rel="noopener noreferrer"&gt; Amazon Comprehend&lt;/a&gt; to use natural-language-processing (NLP) to perform entity extraction and sentiment analysis.&lt;br&gt;
&lt;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.amazonaws.com%2Fuploads%2Farticles%2Fcpxut2karnrv6zjf7hfk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcpxut2karnrv6zjf7hfk.png" alt="Usecase Architecture" width="800" height="432"&gt;&lt;/a&gt;&lt;br&gt;
A second Kinesis Data Firehose delivery stream loads the translated tweets and sentiment values into the &lt;em&gt;sentiment&lt;/em&gt; prefix in the Amazon S3 bucket. A third delivery stream loads &lt;em&gt;entities&lt;/em&gt; in the entities prefix using in the Amazon S3 bucket.&lt;br&gt;
This solution uses a data lake leveraging &lt;a href="https://aws.amazon.com/glue/" rel="noopener noreferrer"&gt;AWS Glue&lt;/a&gt; for data transformation,&lt;a href="https://aws.amazon.com/athena/" rel="noopener noreferrer"&gt; Amazon Athena&lt;/a&gt; for data analysis, and&lt;a href="https://aws.amazon.com/quicksight/" rel="noopener noreferrer"&gt; Amazon QuickSight&lt;/a&gt; for data visualization. AWS Glue Data Catalog contains a logical database which is used to organize the tables for the data on Amazon S3. Athena uses these table definitions to query the data stored on Amazon S3 and return the information to an Amazon QuickSight dashboard.&lt;br&gt;
On the other hand, if you use either the &lt;em&gt;ML services&lt;/em&gt; or &lt;em&gt;ML frameworks and infrastructure&lt;/em&gt;, we recommend you implement an MLOps solution regardless of the use case. The &lt;em&gt;ML services&lt;/em&gt; stack’s ease of use and support for various use cases makes it desirable for implementing any  ML-based pipeline.&lt;br&gt;
Also, since different model training and serving algorithms can alter aspects of the MLOps solution, there are three main options Amazon SageMaker provides when it comes to choosing your training algorithm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a built-in Amazon SageMaker algorithm or framework.&lt;/strong&gt; With this option, a training dataset is the only input developers and data scientists have to provide when training their models. On the other hand, the trained model artefacts are the only input they need to deploy the models. This is a good fit for scenarios where off-the-shelf solutions serve the purpose. .&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use pre-built Amazon SageMaker container images.&lt;/strong&gt; For this option, you need to provide two inputs to train your models, and they are your training scripts and datasets. Likewise, the inputs for deploying the trained models are your serving scripts and the trained model artefacts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extend a pre-built Amazon SageMaker container image, or adapt an existing container image.&lt;/strong&gt; This is for more advanced use cases. You are responsible for developing and maintaining those container images. Therefore, you may want to consider implementing a CI/CD pipeline to automate the building, testing, and publishing of the customized Amazon SageMaker container images and then integrate the pipeline with your MLOps solution.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;This can introduce some complexity to the MLOps solution, but it provides the flexibility to use custom and other third-party libraries to build and train models. This option’s training inputs are your training scripts, datasets and customized Docker image for Amazon SageMaker. Lastly, the inputs for model deployment are the trained model artefacts, serving scripts and customized Docker image for Amazon SageMaker.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Recommended MLOps Platform :&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This recommended  architecture demonstrates how you can integrate the Amazon SageMaker container image CI/CD pipeline with your ML (training) pipeline.&lt;br&gt;
&lt;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.amazonaws.com%2Fuploads%2Farticles%2F7ca3lpgv6b5w1hy7d9op.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ca3lpgv6b5w1hy7d9op.png" alt="MLOps Recommended Architecture" width="800" height="536"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The components of the MLOps Platform architecture diagram are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A secured development environment was implemented using an Amazon SageMaker Notebook Instance deployed to a custom virtual private cloud (VPC), and secured by implementing security groups and routing the notebook’s internet traffic via the custom VPC. \
Also, the development environment has two Git repositories (AWS CodeCommit) attached: one for the Exploratory Data Analysis (EDA) code and the other for developing the custom Amazon SageMaker Docker container images.&lt;/li&gt;
&lt;li&gt;An ML CI/CD pipeline made up of three sub-components:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Data validation step&lt;/em&gt;&lt;/strong&gt; implemented using AWS Lambda and triggered using AWS CodeBuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Model training/retraining pipeline&lt;/em&gt;&lt;/strong&gt; implemented using Amazon SageMaker Pipelines (pipeline-as-code) and executed using CodeBuild.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Model deployment pipeline&lt;/em&gt;&lt;/strong&gt; that natively supports model rollbacks was implemented using AWS CloudFormation.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, AWS CodePipeline is used to orchestrate the pipeline.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A CI/CD pipeline for developing and deploying the custom Amazon SageMaker Docker container image. This pipeline automatically triggers the ML pipeline when you successfully push a new version of the SageMaker container image, providing the following benefits:

&lt;ul&gt;
&lt;li&gt;Developers and data scientists can thoroughly test and get immediate feedback on the ML pipeline’s performance after publishing a new version of the Docker image. This helps ensure ML pipelines are adequately tested before promoting them to production.&lt;/li&gt;
&lt;li&gt;Developers and data scientists don’t have to manually update the ML pipeline to use the latest version of the customized Amazon SageMaker image when working on the develop git branch. They can branch off the develop branch if they want to use an older version or start developing a new version, which they will merge back to develop branch once approved.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A model monitoring solution implemented using&lt;a href="https://docs.aws.amazon.com/sagemaker/latest/dg/model-monitor.html" rel="noopener noreferrer"&gt; Amazon SageMaker Model Monitor&lt;/a&gt; to monitor the production models’ quality continuously. This provides monitoring for the following: data drift, model drift, the bias in the models’ predictions, and drifts in feature attributes. You can start with the default model monitor, which requires no coding.&lt;/li&gt;
&lt;li&gt;A model retraining implementation that is based on the metric-based model retraining strategy. There are three main retaining strategies available for your model retraining implementation:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Scheduled:&lt;/em&gt;&lt;/strong&gt; This kicks off the model retraining process at a scheduled time and can be implemented using an Amazon EventBridge scheduled event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Event-driven:&lt;/em&gt;&lt;/strong&gt; This kicks off the model retraining process when a new model retraining dataset is made available and can be implemented using an EventBridge event.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;em&gt;Metric-based:&lt;/em&gt;&lt;/strong&gt; This is implemented by creating a Data Drift CloudWatch Alarm that triggers your model retraining process once it goes off, fully automating your correction action for a model drift.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A data platform can be implemented using Amazon S3 buckets, RDS or Feast with versioning enabled.&lt;/li&gt;
&lt;li&gt;A model governance framework, which is not obvious from the architectural diagram and is made of the following components:

&lt;ul&gt;
&lt;li&gt;A model registry for versioning and tracking the trained model artefacts, implemented using Amazon SageMaker Model Registry.&lt;/li&gt;
&lt;li&gt;Dataset versioning implemented using Amazon S3 bucket versioning.&lt;/li&gt;
&lt;li&gt;ML workflow steps auditability, visibility, and reproducibility implemented using Amazon SageMaker Lineage Tracking.&lt;/li&gt;
&lt;li&gt;Secured trained model artefacts implemented using AWS Identity and Access Management (IAM) roles to ensure only authorized individuals have access.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;People in MLOps :&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
  &lt;tbody&gt;
&lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;&lt;em&gt;Rold&lt;/em&gt;&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;strong&gt;&lt;em&gt;Role in ML Life Cycle&lt;/em&gt;&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;strong&gt;&lt;em&gt;MLOps Requirements&lt;/em&gt;&lt;/strong&gt;
   &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;&lt;em&gt;Data Scientists&lt;/em&gt;&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;Build models that address the business question or needs brought by subject matter experts.

&lt;/li&gt;
&lt;li&gt;Deliver operationalizable models so that they can be properly used in the production environment and with production data.

&lt;/li&gt;
&lt;li&gt;Assess model quality (of both original and tests) in tandem with subject matter experts to ensure they answer initial business questions or needs.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;Automated model packaging and delivery for quick and easy deployment to production.

&lt;/li&gt;
&lt;li&gt;Ability to develop tests to determine the quality of deployed models and to make continual improvements.

&lt;/li&gt;
&lt;li&gt;Visibility into the performance of all deployed models from one central location.

&lt;/li&gt;
&lt;li&gt;Ability to investigate data pipelines of each model to make quick assessments and adjustments regardless of who originally built the model.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;&lt;em&gt;Data Engineers&lt;/em&gt;&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;Optimize the retrieval and use of data to power ML models.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;Visibility into performance of all deployed models.

&lt;/li&gt;
&lt;li&gt;Ability to see the full details of individual data pipelines to address underlying data plumbing issues.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;&lt;em&gt;Software Engineers&lt;/em&gt;&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;Integrate ML models in the company’s applications and systems.

&lt;/li&gt;
&lt;li&gt;Ensure that ML models work seamlessly with other non-ML based applications.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;Versioning and automatic tests.

&lt;/li&gt;
&lt;li&gt;The ability to work in parallel on the same application.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;&lt;em&gt;ML Architects&lt;/em&gt;&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;Ensure a scalable and flexible environment for ML model pipelines, from design to development and monitoring.

&lt;/li&gt;
&lt;li&gt;Introduce new technologies when appropriate that improve ML model performance in production.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;High-level overview of models and their resources consumed.

&lt;/li&gt;
&lt;li&gt;Ability to drill down into data pipelines to assess and adjust infrastructure needs.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
   &lt;td&gt;
&lt;strong&gt;&lt;em&gt;SRE&lt;/em&gt;&lt;/strong&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;Conduct and build operational systems and test for security, performance, availability.

&lt;/li&gt;
&lt;li&gt;Continuous Integration/Continuous Delivery pipeline management.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
   &lt;td&gt;
&lt;ul&gt;

&lt;li&gt;Seamless integration of MLOps into the larger DevOps strategy of the enterprise.

&lt;/li&gt;
&lt;li&gt;Seamless deployment pipeline.
&lt;/li&gt;
&lt;/ul&gt;
   &lt;/td&gt;
  &lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;References :&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/solutions/implementations/aws-mlops-framework/" rel="noopener noreferrer"&gt;AWS MLOps Framework | Implementations | AWS Solutions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blogs.halodoc.io/halodoc-ml-platform/" rel="noopener noreferrer"&gt;Harnessing the power of Machine Learning to fuel the growth of Halodoc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@pradeep.natarajan2012/mlops-with-amazon-web-services-754758ecc1df" rel="noopener noreferrer"&gt;MLOPs with Amazon Web Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sagemaker/index.html" rel="noopener noreferrer"&gt;Amazon SageMaker Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.oreilly.com/library/view/introducing-mlops/9781492083283/" rel="noopener noreferrer"&gt;Introducing MLOps [Book]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ml-ops.org/" rel="noopener noreferrer"&gt;ML Ops: Machine Learning Operations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://towardsdatascience.com/a-practical-guide-to-mlops-in-aws-sagemaker-part-i-1d28003f565" rel="noopener noreferrer"&gt;A Practical Guide to MLOps in AWS Sagemaker — Part I&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>machinelearning</category>
      <category>aws</category>
      <category>datascience</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
