<?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>Forem: Emin Mammadov</title>
    <description>The latest articles on Forem by Emin Mammadov (@iameminmammadov).</description>
    <link>https://forem.com/iameminmammadov</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1424685%2F0dfe34b7-1db7-4fc2-9b49-ac2ea7712e92.jpeg</url>
      <title>Forem: Emin Mammadov</title>
      <link>https://forem.com/iameminmammadov</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/iameminmammadov"/>
    <language>en</language>
    <item>
      <title>Setting up Ray on GKE: How I spent a week optimising Docker pulls?</title>
      <dc:creator>Emin Mammadov</dc:creator>
      <pubDate>Sat, 09 May 2026 20:27:03 +0000</pubDate>
      <link>https://forem.com/iameminmammadov/setting-up-ray-on-gke-how-i-spent-a-week-optimising-docker-pulls-13l9</link>
      <guid>https://forem.com/iameminmammadov/setting-up-ray-on-gke-how-i-spent-a-week-optimising-docker-pulls-13l9</guid>
      <description>&lt;p&gt;I spent a week debugging slow Ray cluster starts on GKE. The fix was a region mismatch that is not very obvious from the docs. &lt;/p&gt;

&lt;p&gt;We've been running Ray on GKE (with Anyscale) for over a year on the AI Platform team at Geotab. As self-hosted LLM workloads grow, Ray is one of the tools that makes scaling them practical. Introducing Ray and making it a go-to platform for multiple teams has been a rewarding but challenging path. One issue I kept running into: slow Ray cluster spawn times. Here's where the time actually went, and what helped.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. GKE node provisioning: 2-3 minutes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When Ray's autoscaler asks for a new node, GKE has to allocate a VM, boot the OS, register the kubelet, and join the cluster. GPU nodes add another 30-50 seconds for driver install. We treated this as a baseline cost - no point optimizing anything else until the node exists. That recently changed a bit as GCP introduced GKE Active Buffer that aims to minimize that time. I haven't tested it yet, but it's on the list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Image pull: 10+ minutes (and where I lost a week)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Ray + ML container images are big. An LLM-flavored image hits 10-15 GB easily; even a classic CV image with PyTorch lands at 13 GB+. Pulling that fresh on every new node was 15-20 minutes.&lt;/p&gt;

&lt;p&gt;GKE Image Streaming is supposed to fix this as containers start before the full image is pulled. However, even after enabling it, pulls were still occasionally taking 20+ minutes.&lt;/p&gt;

&lt;p&gt;What made this brutal to debug:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It didn't fail consistently. Users didn't always report it.&lt;/li&gt;
&lt;li&gt;Anyscale assigns new pod names on each restart, so by the time I went looking, the original pod was gone and pulling logs on pod level was impossible.&lt;/li&gt;
&lt;li&gt;The log volume is high. Without precise timestamps, finding the relevant entries is painful.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The detail not obvious from the docs: &lt;strong&gt;Image Streaming requires your Artifact Registry repo to be in the same region as your GKE nodes.&lt;/strong&gt; Cluster in &lt;code&gt;us-central1&lt;/code&gt; + repo in the &lt;code&gt;us&lt;/code&gt; multi-region doesn’t enable streaming and it silently falls back to a normal pull. &lt;/p&gt;

&lt;p&gt;The very first step is to ensure that Image Streaming is actually enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud container clusters describe &amp;lt;cluster_name&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;control_plane_location&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;--flatten&lt;/span&gt; &lt;span class="s2"&gt;"nodePoolDefaults.nodeConfigDefaults"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;gsfsConfig&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To verify it's actually engaging on a specific node, this Cloud Logging query was what cracked the case for me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;resource.type="k8s_node"&lt;/span&gt;
&lt;span class="s"&gt;resource.labels.node_name="&amp;lt;name_of_node&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs showed Image Streaming was enabled but not engaging, which led me to the regional requirement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Disk speed on the nodes themselves&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once images do pull, they have to land on disk. We were using HDD-backed nodes. Switching to SSD cut Docker load time by ~30% and brought total spawn time from 15-20 minutes down to 5-6.&lt;/p&gt;

&lt;p&gt;Unglamorous, but worth checking. If you're on HDD, you're paying for it on every cold start.&lt;/p&gt;

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

&lt;p&gt;If your Ray cluster spawns feel slow on GKE, the diagnostic order I'd suggest:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm Image Streaming is actually engaging (don't trust "enabled" - check logs).&lt;/li&gt;
&lt;li&gt;Verify your Artifact Registry region matches your cluster region.&lt;/li&gt;
&lt;li&gt;Check what disk type your node pool is using.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>llmops</category>
      <category>ray</category>
      <category>kubernetes</category>
      <category>gcp</category>
    </item>
    <item>
      <title>Distributed Model Serving Patterns</title>
      <dc:creator>Emin Mammadov</dc:creator>
      <pubDate>Sat, 05 Apr 2025 20:33:07 +0000</pubDate>
      <link>https://forem.com/iameminmammadov/distributed-model-serving-patterns-43ml</link>
      <guid>https://forem.com/iameminmammadov/distributed-model-serving-patterns-43ml</guid>
      <description>&lt;h2&gt;
  
  
  Intro
&lt;/h2&gt;

&lt;p&gt;The goal of every company is to make money and AI Models are seen as an integral part of the business. As machine learning models move from experimentation to production, serving them becomes a challenge. Serving them at a scale becomes even a larger issue. Having a model that shows high accuracy isn't enough - we need an infrastructure that will be robust, efficient, and scalable. In this article, I will dive deeper into main model serving patterns. This would be useful for anyone building model ML Platform systems that need to operate reliably despite large number of users (and requests) or large data.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Model Serving?
&lt;/h2&gt;

&lt;p&gt;Model serving is the process of loading a previously trained machine learning model with the ultimate goal of generating predictions or in general, perform inference on new and unseen data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replicated Services Pattern
&lt;/h2&gt;

&lt;p&gt;Imagine a very simple prediction server. You have a use case where users upload their photos or videos, and the trained ML model will automatically label people in them. In general, any API should be stateless; each request is processed independently and is treated as a completely new transaction without knowing anything about a client. On a small-scale, it would be possible to run predictions with a single node. However, a growing number of user requests will inevitably lead to delays in getting responses, as those requests will be processed in sequence. To solve this bottleneck, &lt;strong&gt;Replicated Services Pattern&lt;/strong&gt; is used.&lt;/p&gt;

&lt;p&gt;The Replicated Services pattern is what is typically meant when discussing Web Server Scalability 101. The core concept here is adding multiple instances of the same model server; an instance here is a copy (or replica) of the original web server with a different address. As an API is stateless, adding or reducing equivalent servers (AKA Horizontal Scaling) allows for seamless scaling of inference and ensures High Availability (HA). To ensure that requests go to appropriate servers &lt;strong&gt;Load Balancers&lt;/strong&gt; are deployed. &lt;/p&gt;

&lt;p&gt;Additionally to the information above, Replicated Services pattern helps with reducing latency as replicas can be put closer to a client's geographic location, and thus, minimizing latency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharded Services Pattern
&lt;/h2&gt;

&lt;p&gt;In the previous pattern, the goal was to distribute many requests to ensure that clients get responses fast. However, it's very common to have large datasets when it comes to inference in ML domain. An expectation of serving large amounts of data is one of the core differences between a regular web server and a web server designed for ML inferences. Thus, it is common to rely on yet another serving pattern, called &lt;strong&gt;Sharded Services Pattern&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Replicated Services Pattern has fixed, identical computational resources. Regular web servers are not expected to do computationally-intensive tasks but it's something that is expected out of ML-specific ones and thus, those computational resources would be a bottleneck. In Sharded Services pattern, large request is divided into smaller pieces, where each piece (or segment) is processed independently by a &lt;strong&gt;model server shard&lt;/strong&gt;, which is a partition of a larger model server. After processing each segment, the results are merged into a final output. &lt;/p&gt;

&lt;p&gt;Sharding Services pattern is useful not only with large datasets but also in use cases where each shard is responsible for a specific task (Natural Language Processing on one shard and Computer Vision model on another). Another use case would include shards accounting for certain data characteristics such as geographic regions.&lt;/p&gt;

&lt;p&gt;One of the core concepts of this pattern is &lt;strong&gt;sharding function&lt;/strong&gt;. A sharding function acts as an intelligent router that determines which shard process a sub-request should go to. Sharding function is conceptually very similar to &lt;strong&gt;hash functions&lt;/strong&gt; used in more traditional distributed applications. Important characteristics of sharding functions would be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Uniform distribution: It is important to distribute load evenly to prevent "hot shards" that become overloaded while others are underutilized. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Minimal resharding impact: If the number of shards changes, the workload redistribution should be minimized. This is conceptually similar to consistent hashing algorithms like Ring Hash.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Context Awareness: For ML workloads specifically, the sharding function will need to understand model-specific characteristics and routing must be done based on input size, computational complexity, and/or data characteristics.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It would be important to note that a load balancer would employ more stateful algorithms that will provide some information about the client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Event-Driven Processing Pattern
&lt;/h2&gt;

&lt;p&gt;Event-driven Processing Pattern compliments the previous ones. In this pattern, the system reacts on demand, allocating resources only when requests arrive. In this model, the system operates on demand, allocating resources only when inference requests arrive rather than maintaining constantly active services. This approach leverages a shared resource pool where compute capacity is dynamically borrowed based on current load requirements, enabling efficient utilization across the entire infrastructure. A critical consideration in this architecture is implementing robust defenses against denial-of-service attacks, as both accidental (buggy clients) and malicious overuse can potentially overwhelm the system. Protection mechanisms typically include rate limiting to control request processing velocity, along with intelligent queuing systems that buffer excess requests and process them at a manageable pace without losing data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Hope you enjoyed reading this post and in the future ones, I will dive deeper into the rest of Machine Learning Infrastructure setup.&lt;/p&gt;

</description>
      <category>distributedsystems</category>
      <category>mlops</category>
      <category>machinelearning</category>
      <category>dataengineering</category>
    </item>
  </channel>
</rss>
