<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ NVIDIA - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ NVIDIA - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 28 Jul 2026 17:16:31 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/nvidia/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ Beyond NVIDIA: Where the AI Infra Trade Actually Shows Up ]]>
                </title>
                <description>
                    <![CDATA[ The AI capex trade is usually discussed like one clean idea. Capex simply means capital expenditure, or the money companies spend on long-term assets like data centers, chips, servers, power systems,  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/beyond-nvidia-where-the-ai-infra-trade-actually-shows-up/</link>
                <guid isPermaLink="false">6a1a129da369e7c9ad1b3aa9</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ infrastructure ]]>
                    </category>
                
                    <category>
                        <![CDATA[ stocks ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NVIDIA ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nikhil Adithyan ]]>
                </dc:creator>
                <pubDate>Fri, 29 May 2026 22:26:37 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/31d12d22-a89b-44c1-8786-ef568be6e6b8.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>The AI capex trade is usually discussed like one clean idea. Capex simply means capital expenditure, or the money companies spend on long-term assets like data centers, chips, servers, power systems, and other infrastructure.</p>
<p>NVIDIA. Hyperscalers. Data centers. Power demand. Everything gets pushed into the same bucket and called "AI infrastructure."</p>
<p>But I don't think this is very useful anymore.</p>
<p>Capex doesn't move through the market as a headline. It moves through a chain. A cloud company decides to spend more on AI infrastructure, but that spending has to pass through chips, semiconductor equipment, servers, networking, data centers, power systems, cooling, and construction before it becomes usable compute.</p>
<p>That's where the story gets more interesting.</p>
<p>The obvious AI names still matter, but they're not the whole map. If AI capex is becoming one of the biggest investment cycles in the market, then the better question isn't just:</p>
<blockquote>
<p><em>"Which companies are AI stocks?"</em></p>
</blockquote>
<p>It's actually:</p>
<blockquote>
<p><em>"Where does the money actually travel?"</em></p>
</blockquote>
<p>In this article, we'll use Python and <a href="https://eodhd.com/">EODHD</a> data to build a simple AI capex map. The goal isn't to create a buy list. The goal is to separate the theme into layers, compare fundamentals with market recognition, and see where the AI infrastructure trade is already showing up in the data.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-were-investigating">What We're Investigating</a></p>
</li>
<li><p><a href="#heading-import-the-required-packages">Import the Required Packages</a></p>
</li>
<li><p><a href="#heading-building-the-ai-capex-universe">Building the AI Capex Universe</a></p>
</li>
<li><p><a href="#heading-pulling-the-financial-data-behind-the-story">Pulling the Financial Data Behind the Story</a></p>
<ul>
<li><p><a href="#heading-fundamentals-data">Fundamentals Data</a></p>
</li>
<li><p><a href="#heading-historical-prices-data">Historical Prices Data</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-separating-business-strength-from-market-recognition">Separating Business Strength from Market Recognition</a></p>
<ul>
<li><p><a href="#heading-fundamental-signal">Fundamental Signal</a></p>
</li>
<li><p><a href="#heading-market-recognition-signal">Market Recognition Signal</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-the-ai-capex-matrix-where-the-trade-actually-shows-up">The AI Capex Matrix: Where the Trade Actually Shows Up</a></p>
</li>
<li><p><a href="#which-ai-infrastructure-layers-has-the-market-rewarded-most">Which AI Infrastructure Layers Has the Market Rewarded Most?</a></p>
</li>
<li><p><a href="#heading-the-physical-infrastructure-layer-is-no-longer-hidden">The Physical Infrastructure Layer Is No Longer Hidden</a></p>
</li>
<li><p><a href="#heading-what-the-market-has-already-noticed">What the Market Has Already Noticed</a></p>
</li>
<li><p><a href="#heading-what-this-study-shows">What This Study Shows</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites"><strong>Prerequisites</strong></h2>
<p>Before following along, you should be comfortable with basic Python, especially working with dictionaries, lists, functions, and pandas DataFrames.</p>
<p>You’ll also need:</p>
<ul>
<li><p>Python 3.9 or later</p>
</li>
<li><p>An EODHD API key</p>
</li>
<li><p>The following Python libraries: <code>requests</code>, <code>pandas</code>, <code>numpy</code>, and <code>matplotlib</code></p>
</li>
<li><p>Basic familiarity with financial metrics like revenue growth, profit margin, P/E ratio, stock returns, volatility, and drawdown</p>
</li>
</ul>
<p>You don’t need advanced finance knowledge for this article. The goal is to show how data visualization can help map a market theme, not to build a complete valuation model or stock recommendation engine.</p>
<h2 id="heading-what-were-investigating">What We're Investigating</h2>
<p>The lazy version of this article would be a list of AI stocks.</p>
<p>That's not what I want to do here.</p>
<p>The more useful approach is to treat AI capex as a spending chain and ask where each part of that chain appears in the market.</p>
<p>A company selling GPUs is exposed to the theme in one way. A company building electrical systems for data centers is exposed in a completely different way. Both can benefit from the same capex cycle, but the economics, margins, valuation, and market behavior may look very different.</p>
<p>So the investigation has three parts.</p>
<p>First, we'll create a working AI infrastructure universe across layers like chips, semiconductor equipment, servers, networking, data centers, power, cooling, and construction.</p>
<p>Second, we'll pull fundamentals and price data from EODHD to measure two things:</p>
<ul>
<li><p><strong>Fundamental signal:</strong> Is the business showing growth and profitability?</p>
</li>
<li><p><strong>Market recognition signal:</strong> Has the stock already been rewarded by the market?</p>
</li>
</ul>
<p>Third, we'll map the companies into a matrix and look for patterns.</p>
<p>The main output isn't a ranking of the "best AI infrastructure stocks." It's a clearer view of where the AI capex trade has already shown up, where it looks concentrated, and where the physical infrastructure layer starts becoming hard to ignore.</p>
<h2 id="heading-import-the-required-packages">Import the Required Packages</h2>
<p>We'll keep the setup light. This is an analysis notebook, not a production system.</p>
<pre><code class="language-python">import requests
import pandas as pd
import numpy as np
from datetime import date, timedelta
import matplotlib.pyplot as plt
</code></pre>
<p>These packages cover everything we need here.</p>
<p><code>requests</code> will call the EODHD API, <code>pandas</code> will handle the tables, and <code>numpy</code> will help with basic calculations. We'll use <code>date</code> and <code>timedelta</code> for the one-year price window, and <code>matplotlib</code> for the charts.</p>
<h2 id="heading-building-the-ai-capex-universe">Building the AI Capex Universe</h2>
<p>There's one issue with analyzing AI infrastructure stocks: AI capex exposure isn't a clean financial field.</p>
<p>No API directly tells us that a company is "30% exposed to AI data center spending" or "highly tied to GPU infrastructure." So we need a research universe first.</p>
<p>For this article, I used an LLM as a research assistant to draft the first version of the AI capex chain, then manually reviewed the companies before pulling fundamentals and price data from EODHD.</p>
<p>The universe is split into layers:</p>
<ul>
<li><p>Demand-side hyperscalers</p>
</li>
<li><p>AI compute and chips</p>
</li>
<li><p>Semiconductor equipment</p>
</li>
<li><p>Servers and storage</p>
</li>
<li><p>Networking</p>
</li>
<li><p>Data centers</p>
</li>
<li><p>Power and electrification</p>
</li>
<li><p>Cooling and industrial systems</p>
</li>
<li><p>Construction and engineering</p>
</li>
</ul>
<pre><code class="language-python">ai_capex_universe = [
    {'ticker': 'MSFT.US', 'company': 'Microsoft', 'capex_layer': 'Demand-side hyperscalers', 'exposure_level': 'High', 'reason': 'Major cloud and AI infrastructure spender through Azure'},
    {'ticker': 'AMZN.US', 'company': 'Amazon', 'capex_layer': 'Demand-side hyperscalers', 'exposure_level': 'High', 'reason': 'Large AI and cloud infrastructure spender through AWS'},
    {'ticker': 'GOOGL.US', 'company': 'Alphabet', 'capex_layer': 'Demand-side hyperscalers', 'exposure_level': 'High', 'reason': 'Major AI infrastructure spender across Google Cloud and internal AI systems'},
    {'ticker': 'META.US', 'company': 'Meta Platforms', 'capex_layer': 'Demand-side hyperscalers', 'exposure_level': 'High', 'reason': 'Large AI compute and data center spending program'},

    {'ticker': 'NVDA.US', 'company': 'NVIDIA', 'capex_layer': 'AI compute and chips', 'exposure_level': 'Very High', 'reason': 'Core GPU and accelerator supplier for AI training and inference'},
    {'ticker': 'AMD.US', 'company': 'Advanced Micro Devices', 'capex_layer': 'AI compute and chips', 'exposure_level': 'High', 'reason': 'AI accelerator and data center CPU exposure'},
    {'ticker': 'AVGO.US', 'company': 'Broadcom', 'capex_layer': 'AI compute and chips', 'exposure_level': 'High', 'reason': 'Custom silicon and networking exposure for AI infrastructure'},
    {'ticker': 'MRVL.US', 'company': 'Marvell Technology', 'capex_layer': 'AI compute and chips', 'exposure_level': 'High', 'reason': 'Custom silicon, networking, and data infrastructure exposure'},

    {'ticker': 'AMAT.US', 'company': 'Applied Materials', 'capex_layer': 'Semiconductor equipment', 'exposure_level': 'High', 'reason': 'Supplies equipment used in advanced chip manufacturing'},
    {'ticker': 'LRCX.US', 'company': 'Lam Research', 'capex_layer': 'Semiconductor equipment', 'exposure_level': 'High', 'reason': 'Semiconductor manufacturing equipment supplier'},
    {'ticker': 'KLAC.US', 'company': 'KLA', 'capex_layer': 'Semiconductor equipment', 'exposure_level': 'High', 'reason': 'Process control and inspection tools for chip manufacturing'},
    {'ticker': 'ASML.US', 'company': 'ASML', 'capex_layer': 'Semiconductor equipment', 'exposure_level': 'Very High', 'reason': 'Critical lithography equipment supplier for advanced chips'},

    {'ticker': 'DELL.US', 'company': 'Dell Technologies', 'capex_layer': 'Servers and storage', 'exposure_level': 'High', 'reason': 'AI server and enterprise hardware exposure'},
    {'ticker': 'HPE.US', 'company': 'Hewlett Packard Enterprise', 'capex_layer': 'Servers and storage', 'exposure_level': 'Medium', 'reason': 'Server, storage, and enterprise infrastructure exposure'},
    {'ticker': 'SMCI.US', 'company': 'Super Micro Computer', 'capex_layer': 'Servers and storage', 'exposure_level': 'High', 'reason': 'AI server systems and data center hardware exposure'},

    {'ticker': 'ANET.US', 'company': 'Arista Networks', 'capex_layer': 'Networking', 'exposure_level': 'High', 'reason': 'Data center networking supplier tied to AI cluster buildouts'},
    {'ticker': 'CSCO.US', 'company': 'Cisco', 'capex_layer': 'Networking', 'exposure_level': 'Medium', 'reason': 'Networking and enterprise infrastructure exposure'},

    {'ticker': 'EQIX.US', 'company': 'Equinix', 'capex_layer': 'Data centers', 'exposure_level': 'Medium', 'reason': 'Global data center and interconnection infrastructure'},
    {'ticker': 'DLR.US', 'company': 'Digital Realty', 'capex_layer': 'Data centers', 'exposure_level': 'Medium', 'reason': 'Data center real estate exposure'},

    {'ticker': 'VRT.US', 'company': 'Vertiv', 'capex_layer': 'Power and electrification', 'exposure_level': 'High', 'reason': 'Power and thermal infrastructure for data centers'},
    {'ticker': 'ETN.US', 'company': 'Eaton', 'capex_layer': 'Power and electrification', 'exposure_level': 'Medium', 'reason': 'Electrical systems and power management exposure'},
    {'ticker': 'PWR.US', 'company': 'Quanta Services', 'capex_layer': 'Power and electrification', 'exposure_level': 'Medium', 'reason': 'Grid, power, and infrastructure construction exposure'},
    {'ticker': 'CEG.US', 'company': 'Constellation Energy', 'capex_layer': 'Power and electrification', 'exposure_level': 'Medium', 'reason': 'Power demand beneficiary from data center expansion'},

    {'ticker': 'TT.US', 'company': 'Trane Technologies', 'capex_layer': 'Cooling and industrial systems', 'exposure_level': 'Medium', 'reason': 'Cooling and climate systems exposure for buildings and infrastructure'},
    {'ticker': 'CARR.US', 'company': 'Carrier Global', 'capex_layer': 'Cooling and industrial systems', 'exposure_level': 'Medium', 'reason': 'Cooling, HVAC, and infrastructure systems exposure'},
    {'ticker': 'JCI.US', 'company': 'Johnson Controls', 'capex_layer': 'Cooling and industrial systems', 'exposure_level': 'Medium', 'reason': 'Building systems, controls, and cooling infrastructure exposure'},

    {'ticker': 'EME.US', 'company': 'EMCOR Group', 'capex_layer': 'Construction and engineering', 'exposure_level': 'Medium', 'reason': 'Electrical and mechanical construction exposure'},
    {'ticker': 'FIX.US', 'company': 'Comfort Systems USA', 'capex_layer': 'Construction and engineering', 'exposure_level': 'Medium', 'reason': 'Mechanical and electrical services for commercial infrastructure'}
]

universe = pd.DataFrame(ai_capex_universe)

universe.head()
</code></pre>
<p>This gives us the research universe.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/3c82c07a-f9fa-4d23-aba7-c17d62158589.png" alt="AI capex stock universe (Image by Author)" style="display:block;margin:0 auto" width="1500" height="325" loading="lazy">

<p>The important thing is that this table doesn't prove anything by itself. It only defines the map. The actual comparison comes from the fundamentals and historical price data we pull next.</p>
<h2 id="heading-pulling-the-financial-data-behind-the-story">Pulling the Financial Data Behind the Story</h2>
<p>The universe gives us the map, but the map is not the analysis.</p>
<p>Now we need actual data behind each company. For that, we'll use EODHD fundamentals and historical prices.</p>
<p>The fundamentals help us check business strength. The price data helps us see whether the market has already recognized the company as part of the AI capex trade.</p>
<h3 id="heading-fundamentals-data">Fundamentals Data</h3>
<p>First, we'll pull fundamentals using <a href="https://eodhd.com/lp/fundamental-data-api">EODHD's fundamentals endpoint</a>.</p>
<pre><code class="language-python">api_key = 'YOUR EODHD API KEY'

def get_fundamentals(ticker):
    url = f'https://eodhd.com/api/fundamentals/{ticker}?api_token={api_key}&amp;fmt=json'
    data = requests.get(url).json()
    return data
</code></pre>
<p><strong>Note:</strong> Replace <code>YOUR EODHD API KEY</code> with your actual EODHD API key.</p>
<p>This function calls the fundamentals endpoint for one ticker and returns the full JSON response.</p>
<p>We don't need the entire response for this analysis, so we'll extract only the fields we care about.</p>
<pre><code class="language-python">def extract_fundamental_fields(ticker, data):
    general = data.get('General', {})
    highlights = data.get('Highlights', {})
    valuation = data.get('Valuation', {})
    technicals = data.get('Technicals', {})

    return {
        'ticker': ticker,
        'sector': general.get('Sector'),
        'industry': general.get('Industry'),
        'market_cap': highlights.get('MarketCapitalization'),
        'revenue_growth_yoy': highlights.get('QuarterlyRevenueGrowthYOY'),
        'profit_margin': highlights.get('ProfitMargin'),
        'operating_margin': highlights.get('OperatingMarginTTM'),
        'return_on_equity': highlights.get('ReturnOnEquityTTM'),
        'pe_ratio': highlights.get('PERatio'),
        'forward_pe': valuation.get('ForwardPE'),
        'beta': technicals.get('Beta')
    }
</code></pre>
<p>These fields give us a compact view of growth, profitability, valuation, and company context.</p>
<p>Now we can run this across the full universe.</p>
<pre><code class="language-python">fundamental_rows = []

for ticker in universe['ticker']:
    try:
        data = get_fundamentals(ticker)
        row = extract_fundamental_fields(ticker, data)
        fundamental_rows.append(row)
        print(f'{ticker} DONE')

    except Exception as e:
        fundamental_rows.append({
            'ticker': ticker,
            'sector': np.nan,
            'industry': np.nan,
            'market_cap': np.nan,
            'revenue_growth_yoy': np.nan,
            'profit_margin': np.nan,
            'operating_margin': np.nan,
            'return_on_equity': np.nan,
            'pe_ratio': np.nan,
            'forward_pe': np.nan,
            'beta': np.nan
        })
        print(f'{ticker} ERROR')

fundamentals = pd.DataFrame(fundamental_rows)

fundamentals.head()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/c3430710-8978-4739-9217-b8b729d17c68.png" alt="Fundamentals Data (Image by Author)" style="display:block;margin:0 auto" width="1500" height="353" loading="lazy">

<p>The try block keeps the scan moving if one ticker fails. That matters because this universe mixes different types of companies, and one missing response should not break the whole analysis.</p>
<h3 id="heading-historical-prices-data">Historical Prices Data</h3>
<p>Next, we'll pull one year of historical prices using <a href="https://eodhd.com/lp/historical-eod-api">EODHD's historical end-of-day prices endpoint</a>.</p>
<pre><code class="language-python">price_start = date.today() - timedelta(days=365)
price_end = date.today()

def get_price_history(ticker):
    url = f'https://eodhd.com/api/eod/{ticker}?api_token={api_key}&amp;fmt=json&amp;from={price_start.isoformat()}&amp;to={price_end.isoformat()}&amp;period=d'
    data = requests.get(url).json()
    prices = pd.DataFrame(data)

    if prices.empty:
        return pd.DataFrame()

    prices['date'] = pd.to_datetime(prices['date'], errors='coerce')
    prices['adjusted_close'] = pd.to_numeric(prices['adjusted_close'], errors='coerce')

    prices = prices.dropna(subset=['date', 'adjusted_close'])
    prices = prices.sort_values('date').reset_index(drop=True)

    return prices[['date', 'adjusted_close']]
</code></pre>
<p>We use adjusted close because it's cleaner for return calculations after splits and dividends.</p>
<p>Now we'll convert the price history into a few market signals.</p>
<pre><code class="language-python">def calculate_market_signals(prices):
    if prices.empty or len(prices) &lt; 60:
        return {
            'return_1y': np.nan,
            'return_6m': np.nan,
            'return_3m': np.nan,
            'volatility_1y': np.nan,
            'max_drawdown_1y': np.nan
        }

    prices = prices.copy()
    prices['daily_return'] = prices['adjusted_close'].pct_change()

    latest_close = prices['adjusted_close'].iloc[-1]

    return_1y = (latest_close / prices['adjusted_close'].iloc[0]) - 1
    return_6m = (latest_close / prices['adjusted_close'].iloc[-126]) - 1 if len(prices) &gt;= 126 else np.nan
    return_3m = (latest_close / prices['adjusted_close'].iloc[-63]) - 1 if len(prices) &gt;= 63 else np.nan

    volatility_1y = prices['daily_return'].std() * np.sqrt(252)

    running_high = prices['adjusted_close'].cummax()
    drawdown = (prices['adjusted_close'] / running_high) - 1
    max_drawdown_1y = drawdown.min()

    return {
        'return_1y': return_1y,
        'return_6m': return_6m,
        'return_3m': return_3m,
        'volatility_1y': volatility_1y,
        'max_drawdown_1y': max_drawdown_1y
    }
</code></pre>
<p>These signals tell us how strongly the market has already responded to each company.</p>
<p>Now we run the same logic for every ticker.</p>
<pre><code class="language-python">market_rows = []

for ticker in universe['ticker']:
    try:
        prices = get_price_history(ticker)
        signals = calculate_market_signals(prices)
        signals['ticker'] = ticker
        market_rows.append(signals)
        print(f'{ticker} DONE')

    except Exception:
        market_rows.append({
            'ticker': ticker,
            'return_1y': np.nan,
            'return_6m': np.nan,
            'return_3m': np.nan,
            'volatility_1y': np.nan,
            'max_drawdown_1y': np.nan
        })
        print(f'{ticker} ERROR')

market_signals = pd.DataFrame(market_rows)

market_signals.head()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/3f270de4-2a76-4419-99e6-f6ab322b66f6.png" alt="Market Signals (Image by Author)" style="display:block;margin:0 auto" width="1000" height="311" loading="lazy">

<p>Finally, we merge the universe, fundamentals, and market signals into one dataset.</p>
<pre><code class="language-python">capex_data = universe.merge(fundamentals, on='ticker', how='left')
capex_data = capex_data.merge(market_signals, on='ticker', how='left')

print(capex_data.columns)
capex_data.head()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/eee12f1d-846a-4d08-a968-e2cec4b538a9.png" alt="Capex data columns (Image by Author)" style="display:block;margin:0 auto" width="1000" height="173" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/5ba2a827-f946-4347-a914-f5c30c84cba0.png" alt="Capex data (Image by Author)" style="display:block;margin:0 auto" width="1500" height="643" loading="lazy">

<h2 id="heading-separating-business-strength-from-market-recognition">Separating Business Strength from Market Recognition</h2>
<p>Now comes the part that makes the analysis useful.</p>
<p>If we only look at stock returns, we end up chasing what already moved. If we only look at fundamentals, we miss how the market is actually treating the theme.</p>
<p>So I split the analysis into two simple signals:</p>
<ul>
<li><p><strong>Fundamental Signal:</strong> is the business showing growth and profitability?</p>
</li>
<li><p><strong>Market Recognition Signal:</strong> has the market already rewarded the stock?</p>
</li>
</ul>
<p>First, we need a helper function to normalize each metric.</p>
<pre><code class="language-python">def min_max_score(series):
    series = pd.to_numeric(series, errors='coerce')

    if series.isna().all():
        return pd.Series(0, index=series.index)

    min_val = series.min()
    max_val = series.max()

    if min_val == max_val:
        return pd.Series(0.5, index=series.index)

    return (series - min_val) / (max_val - min_val)
</code></pre>
<p>This brings every metric into a 0 to 1 range, so growth, margins, returns, and drawdowns can be compared without mixing raw scales.</p>
<h3 id="heading-fundamental-signal">Fundamental Signal</h3>
<p>Now we build the fundamental signal.</p>
<pre><code class="language-python">capex_data['revenue_growth_score'] = min_max_score(capex_data['revenue_growth_yoy'])
capex_data['profit_margin_score'] = min_max_score(capex_data['profit_margin'])
capex_data['operating_margin_score'] = min_max_score(capex_data['operating_margin'])
capex_data['roe_score'] = min_max_score(capex_data['return_on_equity'])

capex_data['fundamental_signal'] = (
    capex_data['revenue_growth_score'] * 0.35 +
    capex_data['operating_margin_score'] * 0.30 +
    capex_data['profit_margin_score'] * 0.20 +
    capex_data['roe_score'] * 0.15
) * 100

capex_data['fundamental_signal'] = capex_data['fundamental_signal'].round(2)
capex_data[['ticker', 'company', 'capex_layer', 'revenue_growth_yoy', 'operating_margin', 'profit_margin', 'return_on_equity', 'fundamental_signal']].sort_values('fundamental_signal', ascending=False).head(10)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/fcbea8d5-dd09-422d-910d-db0d54df9b80.png" alt="Fundamental signal (Image by Author)" style="display:block;margin:0 auto" width="1500" height="452" loading="lazy">

<p>This signal isn't trying to crown the best company. It's just checking whether the business data supports the AI capex story.</p>
<p>In my run, NVIDIA clearly stood out because its revenue growth and margins were on a different level. But the interesting part was not only NVIDIA. Names like KLA, Arista, Broadcom, Microsoft, Meta, Lam Research, Alphabet, and Super Micro also appeared near the top for different reasons.</p>
<p>That already tells us something important: the AI capex chain has different types of winners. Some are high-margin platform businesses. Some are semiconductor equipment names. Some are high-growth hardware names with thinner margins.</p>
<h3 id="heading-market-recognition-signal">Market Recognition Signal</h3>
<p>Now we build the market recognition signal.</p>
<pre><code class="language-python">capex_data['return_1y_score'] = min_max_score(capex_data['return_1y'])
capex_data['return_6m_score'] = min_max_score(capex_data['return_6m'])
capex_data['return_3m_score'] = min_max_score(capex_data['return_3m'])
capex_data['drawdown_score'] = min_max_score(capex_data['max_drawdown_1y'])

capex_data['market_recognition_signal'] = (
    capex_data['return_1y_score'] * 0.40 +
    capex_data['return_6m_score'] * 0.30 +
    capex_data['return_3m_score'] * 0.20 +
    capex_data['drawdown_score'] * 0.10
) * 100

capex_data['market_recognition_signal'] = capex_data['market_recognition_signal'].round(2)
capex_data[['ticker','company','capex_layer','return_1y','return_6m','return_3m','max_drawdown_1y','market_recognition_signal']].sort_values('market_recognition_signal', ascending=False).head(10)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/03f91e5a-e63f-49db-95ec-4f718c0c25f0.png" alt="Market recognition signal (Image by Author)" style="display:block;margin:0 auto" width="1500" height="479" loading="lazy">

<p>This is where the story gets more interesting.</p>
<p>The market recognition list wasn't just filled with hyperscalers or chip names. Comfort Systems, Vertiv, Quanta Services, Dell, Applied Materials, and Lam Research showed up strongly. That is the first clear sign that the AI capex trade is spreading into the physical infrastructure layer, not staying locked inside the usual mega-cap AI basket.</p>
<h2 id="heading-the-ai-capex-matrix-where-the-trade-actually-shows-up">The AI Capex Matrix: Where the Trade Actually Shows Up</h2>
<p>At this point, we have two separate lenses.</p>
<ul>
<li><p>The fundamental signal tells us whether the business looks strong.</p>
</li>
<li><p>The market recognition signal tells us whether the stock has already been rewarded.</p>
</li>
</ul>
<p>Now we can put both on the same chart.</p>
<pre><code class="language-python">plt.figure(figsize=(12, 8))

plot_data = capex_data.dropna(
    subset=['market_recognition_signal', 'fundamental_signal', 'market_cap']
).copy()

plot_data['bubble_size'] = np.sqrt(plot_data['market_cap']) / 5000

for layer in plot_data['capex_layer'].unique():
    layer_data = plot_data[plot_data['capex_layer'] == layer]

    plt.scatter(
        layer_data['market_recognition_signal'],
        layer_data['fundamental_signal'],
        s=layer_data['bubble_size'],
        alpha=0.6,
        label=layer
    )

for _, row in plot_data.iterrows():
    if row['market_recognition_signal'] &gt; 55 or row['fundamental_signal'] &gt; 45:
        plt.text(row['market_recognition_signal'] + 0.8, row['fundamental_signal'] + 0.8, row['ticker'].replace('.US', ''), fontsize=10)

plt.axvline(plot_data['market_recognition_signal'].median(), linestyle='--', linewidth=1)
plt.axhline(plot_data['fundamental_signal'].median(), linestyle='--', linewidth=1)

plt.text(median_market + 2, median_fundamental + 55, 'Strong fundamentals,\nmore recognized',fontsize=10)
plt.text(4, median_fundamental + 55,'Strong fundamentals,\nless recognized',fontsize=10)
plt.text(median_market + 2, 4, 'High market recognition,\nweaker fundamentals',fontsize=10)
plt.text(4, 4, 'Less clear in this framework', fontsize=10)

plt.title('AI Capex Matrix: Fundamentals vs Market Recognition')
plt.xlabel('Market Recognition Signal')
plt.ylabel('Fundamental Signal')
plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/ece6d0b1-a270-4e68-9220-d2b50064e691.png" alt="AI Capex Matrix: Fundamentals vs Market Recognition (Image by Author)" style="display:block;margin:0 auto" width="1298" height="866" loading="lazy">

<p>This is the most useful chart in the study.</p>
<p>It makes one thing clear: AI capex doesn't show up in one clean cluster.</p>
<p>NVIDIA is the obvious fundamental outlier. That makes sense. Its growth and margins are difficult to compare with almost anything else in the universe.</p>
<p>But the right side of the chart is where the broader story starts. AMD, Marvell, Vertiv, Comfort Systems, Dell, Lam Research, Applied Materials, and Quanta Services show stronger market recognition. That is a very different mix of companies. Some are chip-related. Some are equipment-related. Some are physical infrastructure names.</p>
<p>That matters because it shows the market isn't only rewarding the most obvious AI companies. It's also rewarding the companies that help turn AI capex into actual infrastructure.</p>
<p>This is the main shift in the article: the AI capex trade starts looking less like a tech basket and more like a buildout chain.</p>
<h2 id="heading-which-ai-infrastructure-layers-has-the-market-rewarded-most">Which AI Infrastructure Layers Has the Market Rewarded Most?</h2>
<p>The matrix is useful at the company level. But the AI capex trade also needs to be viewed by layer.</p>
<p>So next, I grouped the companies by <code>capex_layer</code> and calculated median returns and median signal scores.</p>
<pre><code class="language-python">layer_performance = capex_data.groupby('capex_layer').agg(
    company_count=('ticker', 'count'),
    median_return_1y=('return_1y', 'median'),
    median_return_6m=('return_6m', 'median'),
    median_fundamental_signal=('fundamental_signal', 'median'),
    median_market_recognition=('market_recognition_signal', 'median')
).reset_index()

layer_performance = layer_performance.sort_values('median_return_1y', ascending=False)

layer_performance
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/bfd9fa16-a5e8-4a30-ada3-d707522cdd81.png" alt="Layer performance summary (Image by Author)" style="display:block;margin:0 auto" width="1500" height="433" loading="lazy">

<p>Then I plotted the median one-year return by infrastructure layer.</p>
<pre><code class="language-python">plt.figure(figsize=(11, 6))

plt.barh(layer_performance['capex_layer'], layer_performance['median_return_1y'] * 100)

plt.gca().invert_yaxis()

plt.title('Median 1Y Return by AI Infrastructure Layer', fontsize=14, pad=12)
plt.xlabel('Median 1Y Return (%)')
plt.ylabel('')

plt.grid(axis='x', alpha=0.25)

plt.tight_layout()
plt.show()
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/ed630cda-e8ff-43e3-aa10-a95a9b210a6a.png" alt="Median 1Y Return by AI Infrastructure Layer (Image by Author)" style="display:block;margin:0 auto" width="1410" height="742" loading="lazy">

<p>This chart is where the story becomes much less obvious.</p>
<p>Construction and engineering ranked at the top by median one-year return, followed by semiconductor equipment, AI compute and chips, and servers and storage. That's not the usual way people talk about the AI trade.</p>
<p>The takeaway is not that construction and engineering is automatically the best AI capex layer. The sample size is small, so the result should be read as directional. But it still tells us something useful: the market has been rewarding the physical buildout side of AI infrastructure, not just the companies selling chips or cloud services.</p>
<p>That's the larger point. Once AI capex becomes real-world infrastructure, the trade starts showing up in companies tied to equipment, servers, electrical work, and construction.</p>
<h2 id="heading-the-physical-infrastructure-layer-is-no-longer-hidden">The Physical Infrastructure Layer Is No Longer Hidden</h2>
<p>This is the part of the AI capex trade that I find most useful.</p>
<p>The obvious AI story starts with chips and hyperscalers. But once the spending becomes real infrastructure, the list gets wider. AI data centers need servers, networking equipment, power systems, cooling, grid work, electrical construction, and physical capacity.</p>
<p>So I filtered the dataset to focus on the non-obvious infrastructure layers.</p>
<pre><code class="language-python">physical_layers = ['Power and electrification', 'Cooling and industrial systems', 'Construction and engineering',
                   'Data centers', 'Servers and storage', 'Networking']

physical_infra = capex_data[capex_data['capex_layer'].isin(physical_layers)].copy()
physical_infra = physical_infra.sort_values(['market_recognition_signal', 'fundamental_signal'], ascending=False)
physical_watchlist = physical_infra[['ticker', 'company', 'capex_layer', 'revenue_growth_yoy', 'operating_margin',
                                     'return_1y', 'return_6m', 'fundamental_signal', 'market_recognition_signal']].head(12)

physical_watchlist.head(10)
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/5187a7b4-99de-4f69-8e23-df3a2625ef11.png" alt="Physical infrastructure watchlist (Image by Author)" style="display:block;margin:0 auto" width="1500" height="579" loading="lazy">

<p>Comfort Systems, Vertiv, Dell, Quanta Services, Cisco, HPE, EMCOR, Equinix, Johnson Controls, and Digital Realty all sit in different parts of the physical buildout. Some are tied to servers. Some are tied to power and electrification. Some are tied to data centers, cooling, or construction.</p>
<p>The key point is simple: the market is already treating parts of the physical infrastructure layer as part of the AI capex story.</p>
<p>That doesn't mean every name here has the same quality or the same upside. The fundamental signals vary a lot. But the table shows why looking only at "AI software" or "AI chip" names misses a large part of the spending chain.</p>
<h2 id="heading-what-the-market-has-already-noticed">What the Market Has Already Noticed</h2>
<p>This section is important because not every AI capex name is early.</p>
<p>Some companies in the chain have already moved aggressively. That doesn't make them weak companies, but it changes the question. At that point, the question is no longer just whether the company is exposed to AI infrastructure. The better question is whether the market has already priced in a large part of that exposure.</p>
<p>To check that, I sorted the universe by the market recognition signal.</p>
<pre><code class="language-python">market_already_noticed = capex_data.sort_values('market_recognition_signal', ascending=False).head(10).copy()

market_already_noticed['return_1y'] = (market_already_noticed['return_1y'] * 100).round(2)
market_already_noticed['return_6m'] = (market_already_noticed['return_6m'] * 100).round(2)
market_already_noticed['return_3m'] = (market_already_noticed['return_3m'] * 100).round(2)
market_already_noticed['max_drawdown_1y'] = (market_already_noticed['max_drawdown_1y'] * 100).round(2)

market_already_noticed = market_already_noticed[['ticker', 'company', 'capex_layer', 'return_1y', 'return_6m', 'return_3m', 
                                                 'max_drawdown_1y', 'market_recognition_signal', 'fundamental_signal']]

market_already_noticed
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/5f362fe21017f7317167b14c/2ac645ab-3050-453e-a4cd-a78e0a966030.png" alt="Market already noticed list (Image by Author)" style="display:block;margin:0 auto" width="1500" height="578" loading="lazy">

<p>This list is a useful reality check.</p>
<p>Comfort Systems, AMD, Marvell, Vertiv, Lam Research, Dell, Applied Materials, Quanta Services, Cisco, and Alphabet all show up with strong market recognition. The mix is the important part. It includes chips, semiconductor equipment, servers, networking, power, construction, and a hyperscaler.</p>
<p>That tells us the AI capex trade has already broadened in price action. It's not waiting quietly in the background.</p>
<p>But this also means we need to be careful with the "hidden beneficiary" framing. Some infrastructure names have already delivered very large one-year returns. So the smarter follow-up question is not:</p>
<blockquote>
<p>"Which companies are exposed?"</p>
</blockquote>
<p>It's:</p>
<blockquote>
<p>"How much of that exposure has the market already recognized?"</p>
</blockquote>
<h2 id="heading-what-this-study-shows">What This Study Shows</h2>
<p>The AI capex trade is easier to understand when we stop treating it as one group of "AI stocks."</p>
<p>The data shows three things clearly.</p>
<p>First, the obvious names still matter. NVIDIA remains the cleanest fundamental outlier in this universe, and chip-related names continue to sit close to the center of the AI infrastructure story.</p>
<p>Second, the trade has already moved beyond chips. Semiconductor equipment, servers, networking, power, and construction names all show up in the market recognition data. That makes sense. AI infrastructure isn't just model training. It needs physical capacity, electrical systems, cooling, data centers, and buildout work.</p>
<p>Third, market recognition and business strength don't always move together. Some companies have strong fundamentals but quieter price action. Others have already moved aggressively, even if their fundamental signal isn't as strong. That's why a simple "AI beneficiary" label isn't enough.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>AI capex isn't just a mega-cap tech story. It's a spending chain.</p>
<p>Once we trace that chain, the theme becomes broader and more interesting. It moves from chips to semiconductor equipment, from servers to networking, from data centers to power, cooling, and construction.</p>
<p>The goal of this study wasn't to find the best AI infrastructure stock. It was to build a clearer map of where the trade is already showing up.</p>
<p>That map matters because the next phase of the AI story may not be about who mentions AI the most. It may be about who sits closest to the infrastructure that makes AI possible.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ The Evolution of Nvidia Blackwell GPU Memory Architecture ]]>
                </title>
                <description>
                    <![CDATA[ Each GPU generation pushes against the same constraint: memory. Models grow faster than memory capacity, forcing engineers into complex multi-GPU setups, aggressive quantization, or painful trade-offs ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-evolution-of-nvidia-blackwell-gpu-memory-architecture/</link>
                <guid isPermaLink="false">69e7b761e4367278147e0832</guid>
                
                    <category>
                        <![CDATA[ GPU ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NVIDIA ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NVIDIA B200 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GH200 ]]>
                    </category>
                
                    <category>
                        <![CDATA[ memory ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Rasheedat Atinuke Jamiu ]]>
                </dc:creator>
                <pubDate>Tue, 21 Apr 2026 17:44:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/d2339663-d031-49df-9bfb-90505af532f8.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Each GPU generation pushes against the same constraint: memory. Models grow faster than memory capacity, forcing engineers into complex multi-GPU setups, aggressive quantization, or painful trade-offs.</p>
<p>NVIDIA's Blackwell architecture, succeeding Hopper in 2024, attacks this problem at the hardware level, rethinking not just how much memory a GPU has, but how it's structured and accessed entirely.</p>
<p>Running Llama 3 70B is no longer a concern – no parallelization or squeezing the model into tight memory limits. Instead, the same hardware footprint can now handle significantly larger parameter counts.</p>
<p>This article breaks down the memory enhancements that make Blackwell the most capable AI accelerator to date.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This article assumes you're comfortable with a few GPU fundamentals. If any of these feel shaky, the linked resources will get you up to speed in 10–15 minutes each.</p>
<ul>
<li><p><strong>GPU anatomy</strong> — what an SM is, and the role of registers, shared memory (L1), L2 cache, and memory controllers. [<a href="https://www.arccompute.io/arc-blog/gpu-101-memory-hierarchy">Memory Hierarchy of GPUs</a>]</p>
</li>
<li><p><strong>The three memory metrics</strong> — capacity (how much fits), bandwidth (how fast data moves), and latency (how long a single access takes). These aren't interchangeable, and Blackwell improves all three differently. [<a href="https://www.digitalocean.com/community/tutorials/gpu-memory-bandwidth">GPU Memory Bandwidth]</a></p>
</li>
<li><p><strong>GPU memory types</strong> — HBM, GDDR, and LPDDR5X, and the bandwidth/capacity/power trade-offs between them. [<a href="https://medium.com/@jghaly00/cuda-gpu-memory-types-a07428b3eb16">Cuda GPU Memory Types]</a></p>
</li>
<li><p><strong>Chip interconnects</strong> — PCIe, NVLink, and the idea of a chip-to-chip (C2C) link. [<a href="https://medium.com/@adi.fu7/the-ai-systems-game-are-chip-to-chip-interconnects-the-future-of-inference-ec3bbda53eb3">The AI Systems Game</a>]</p>
</li>
</ul>
<p>If you're solid on all four, you're ready.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-the-generational-leap">The Generational Leap</a></p>
</li>
<li><p><a href="#heading-the-gb200-superchip">The GB200 Superchip</a></p>
<ul>
<li><p><a href="#heading-grace-cpu">Grace CPU</a></p>
</li>
<li><p><a href="#heading-lpddr5x-low-power-double-data-rate-5x">LPDDR5X (Low Power Double Data Rate 5x)</a></p>
</li>
<li><p><a href="#heading-blackwell-gpu">Blackwell GPU</a></p>
</li>
<li><p><a href="#heading-high-bandwidth-interface-nv-hbi">High-Bandwidth Interface (NV-HBI)</a></p>
</li>
<li><p><a href="#heading-nvlink-c-2-c-chip-to-chip">NVLINK C-2-C (Chip-to-Chip)</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-memory-hierarchy-and-bandwidth">Memory Hierarchy and Bandwidth</a></p>
<ul>
<li><p><a href="#heading-the-hierarchy-at-a-glance">The Hierarchy at a Glance</a></p>
</li>
<li><p><a href="#heading-registers-and-l1shared-memory">Registers and L1/Shared Memory</a></p>
</li>
<li><p><a href="#heading-l2-cache-compensating-for-smaller-l1">L2 Cache: Compensating for Smaller L1</a></p>
</li>
<li><p><a href="#heading-hbm3e-the-main-memory-pool">HBM3e: The Main Memory Pool</a></p>
</li>
<li><p><a href="#heading-lpddr5x-the-extended-tier">LPDDR5X: The Extended Tier</a></p>
</li>
<li><p><a href="#heading-data-flow-in-practice">Data Flow in Practice</a></p>
</li>
<li><p><a href="#heading-practical-example-running-llama-3-70b">Practical Example: Running Llama 3 70B</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
<ul>
<li><a href="#heading-references">References</a></li>
</ul>
</li>
</ul>
<h2 id="heading-the-generational-leap">The Generational Leap</h2>
<p>Before diving into how Blackwell achieves its performance gains, here's what changed from the previous GPU generation:</p>
<table>
<thead>
<tr>
<th>Spec</th>
<th>Hopper H100</th>
<th>Blackwell B200</th>
<th>Change</th>
</tr>
</thead>
<tbody><tr>
<td>HBM Capacity</td>
<td>80 GB (HBM3)</td>
<td>192 GB (HBM3e)</td>
<td>2.4×</td>
</tr>
<tr>
<td>HBM Bandwidth</td>
<td>3.35 TB/s</td>
<td>8 TB/s</td>
<td>2.4×</td>
</tr>
<tr>
<td>L2 Cache</td>
<td>50 MB</td>
<td>126 MB</td>
<td>2.5×</td>
</tr>
<tr>
<td>L1/Shared per SM</td>
<td>256 KB</td>
<td>128 KB</td>
<td>0.5×</td>
</tr>
<tr>
<td>Die Design</td>
<td>Monolithic</td>
<td>Dual-die (MCM)</td>
<td>—</td>
</tr>
<tr>
<td>CPU Integration</td>
<td>Separate (PCIe)</td>
<td>Unified (NVLink C2C)</td>
<td>—</td>
</tr>
</tbody></table>
<p>The numbers tell a clear story: more memory, more bandwidth, larger caches. The rest of this article explains how these pieces fit together</p>
<h2 id="heading-the-gb200-superchip">The GB200 Superchip</h2>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769275687108/0c179d24-7f3e-4f63-938c-36723069848c.png" alt="NVIDIA Blackwell GB200 Superchip" style="display:block;margin:0 auto" width="626" height="782" loading="lazy">

<p>The Grace Blackwell (GB200) extends the superchip design NVIDIA introduced with the Grace Hopper (GH200), where an ARM-based Grace CPU is paired with GPU chips in a single package to form one unified computing system.</p>
<p>In the Blackwell generation, the GB200 pairs one Grace CPU with two Blackwell GPUs, connected via NVLink Chip-to-Chip (NVLink-C2C), a high-bandwidth interface that lets the CPU and GPUs share memory and operate as a single system.</p>
<h3 id="heading-grace-cpu">Grace CPU</h3>
<p>The Grace CPU is an ARM Neoverse v2 designed by NVIDIA for bandwidth and efficiency. It handles general-purpose tasks, pre-processing, and tokenization, and feeds data to the GPU through NVLink C-2-C. The Grace CPU acts as extended storage for the GPU.</p>
<p>The Grace CPU runs at a moderate clock speed but compensates with a large memory bandwidth of up to 500GB/s to its LPDDR5X memory (Low Power Double Data Rate 5x – we'll discuss this more in a moment) with about 100MB of L3 Cache.</p>
<h3 id="heading-lpddr5x-low-power-double-data-rate-5x">LPDDR5X (Low Power Double Data Rate 5x)</h3>
<p>The LPDDR5X is a high-speed memory standard that delivers data up to 10.7 Gbps. The LPDDR5X offers low-power efficiency, making it ideal for this use case.</p>
<p>It strikes a perfect balance between performance and power efficiency, delivering up to 500 GB/s while using only about 16W, roughly one-fifth the power of conventional DDR5 memory.</p>
<h3 id="heading-blackwell-gpu">Blackwell GPU</h3>
<p>The Blackwell GPU made significant improvements over the previous Hopper GPU model, especially in terms of memory. The Blackwell GPUs are designed as dual-die GPUs, with two GPU dies in a single module.</p>
<p>Each die is connected by a super-fast NV-HBI (NVIDIA High-Bandwidth Interface) with a speed of 10TB/s, ensuring full performance. Each die contains 104 billion transistors, totaling 208 billion across the two dies. Each die also contains 96 GB of HBM3e memory, totaling 192 GB, with 180 GB usable (as 12 GB is used for error-correcting code (ECC), system firmware, and so on).</p>
<p>With this amount of memory, the Blackwell GPU's memory bandwidth is about 2.4 times faster than that of the Hopper generation.</p>
<p>The L2 cache was also increased to 126 MB. By increasing the L2 cache, Blackwell can store more neural network weights or intermediate results on-chip, avoiding extra trips out to HBM. This ensures the GPU’s compute units are rarely starved for data.</p>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769431323592/8d4d8ba8-dd0c-459e-ad4a-b1a750ffe0d9.png" alt="Blackwell dual-die multichip module (MCM) design" style="display:block;margin:0 auto" width="1011" height="534" loading="lazy">

<h3 id="heading-high-bandwidth-interface-nv-hbi">High-Bandwidth Interface (NV-HBI)</h3>
<p>High Bandwidth Interconnect is a standard for die-to-die (or d2d) communication. The NVIDIA High-Bandwidth Interface (NV-HBI) offers a 10TB/s connection, combining the two GPU dies into a single, unified GPU.</p>
<h3 id="heading-nvlink-c-2-c-chip-to-chip">NVLINK C-2-C (Chip-to-Chip)</h3>
<p>The NVLink C-2-C provides a communication speed of up to ~900 GB/s between the Grace CPU and the Blackwell GPUs, eliminating the need to copy memory from the CPU to the GPU memory pool via the PCIe bus.</p>
<p>The NVLink C-2-C interconnect speed is faster than the typical PCIe bus. PCIe Gen6 is only about 128 GB/s per direction compared to the NVLink C-2-C's speed. It's also cache-coherent, meaning both the CPU and GPU share a coherent memory architecture, allowing the CPU to read and write to GPU memory and vice versa.</p>
<p>This unified memory architecture is called Unified CPU-GPU Memory or Extended GPU Memory (EGM) by NVIDIA.</p>
<h2 id="heading-memory-hierarchy-and-bandwidth">Memory Hierarchy and Bandwidth</h2>
<p>Understanding how data flows through Blackwell's memory system is key to optimizing AI workloads. The architecture follows a classic hierarchy principle: smaller, faster memory sits closest to the compute units, with progressively larger but slower memory tiers extending outward.</p>
<h3 id="heading-the-hierarchy-at-a-glance">The Hierarchy at a Glance</h3>
<table>
<thead>
<tr>
<th>Memory Tier</th>
<th>Capacity</th>
<th>Bandwidth</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td>Registers</td>
<td>~256 KB per SM</td>
<td>Immediate</td>
<td>Active computation</td>
</tr>
<tr>
<td>L1/Shared Memory</td>
<td>~128 KB per SM</td>
<td>~40 TB/s aggregate</td>
<td>Data staging, inter-thread sharing</td>
</tr>
<tr>
<td>L2 Cache</td>
<td>64-65 MB per die (~126 MB total)</td>
<td>~20 TB/s</td>
<td>Cross-SM data reuse</td>
</tr>
<tr>
<td>HBM3e</td>
<td>192 GB (180 usable)</td>
<td>8 TB/s</td>
<td>Model weights, activations</td>
</tr>
<tr>
<td>LPDDR5X (CPU)</td>
<td>~480 GB</td>
<td>~500 GB/s (900 GB/s via NVLink C2C)</td>
<td>Overflow, large embeddings</td>
</tr>
</tbody></table>
<img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769610442328/8d805672-0cb7-4318-bc3d-48b225073fbd.png" alt="Blackwell Memory map" style="display:block;margin:0 auto" width="976" height="748" loading="lazy">

<h3 id="heading-registers-and-l1shared-memory">Registers and L1/Shared Memory</h3>
<p>A streaming multiprocessor (SM) executes compute instructions on the GPU. At the lowest level, each Streaming Multiprocessor (SM) contains a register file and configurable L1/Shared memory as illustrated in the diagram above. Registers hold the operands for active computations, that is, data that the GPU cores are working on right now.</p>
<p>An SM executes threads in fixed-size groups known as <em>warps</em>, with each warp containing exactly 32 threads that execute the same instructions in lockstep. The L1/Shared memory acts as a staging area, allowing threads within an SM to share data without going to slower memory tiers.</p>
<p>Blackwell's L1/Shared memory is 128 KB per SM by default, a reduction from Hopper's 256 KB. In specific configurations, this can extend to 228 KB per SM. The aggregate bandwidth across all SMs is approximately 40 TB/s.</p>
<p>Why the reduction? NVIDIA shifted capacity to TMEM for Tensor Core operations and compensated with a larger L2 cache. General-purpose shared memory workloads see less per-SM capacity, but the workloads that matter most, matrix multiplications, get dedicated, faster memory.</p>
<h3 id="heading-l2-cache-compensating-for-smaller-l1">L2 Cache: Compensating for Smaller L1</h3>
<p>The L2 cache sits between the SMs and HBM, shared across all compute units on a die. Blackwell provides 64-65 MB per die (roughly 126 MB total across the dual-die module). This represents a 2.5× increase over Hopper's 50 MB and compensates for the smaller per-SM L1.</p>
<p>In AI workloads, the same model weights are accessed repeatedly across different input batches. A larger L2 cache means more of these weights can stay on-chip between batches, reducing expensive trips to HBM. For inference serving, where the same model handles thousands of requests, this translates directly to lower latency and higher throughput.</p>
<p>The dual-die design does introduce complexity here. Each die has its own 63 MB L2 partition. Accessing data cached on the other die requires crossing the NV-HBI interconnect fast at 10 TB/s, but still slower than local L2 access. NVIDIA's software stack handles this transparently, but performance-conscious engineers should be aware that data placement across dies can affect cache efficiency.</p>
<h3 id="heading-hbm3e-the-main-memory-pool">HBM3e: The Main Memory Pool</h3>
<p>High Bandwidth Memory (HBM3e) serves as the primary storage for model weights, activations, gradients, and input data. Blackwell's HBM3e delivers 8 TB/s of bandwidth per GPU, roughly 2.4× faster than Hopper's 3.35 TB/s HBM3.</p>
<p>The physical implementation uses an 8-Hi stack design: eight DRAM dies stacked vertically, each providing 3 GB, for 24 GB per stack. With eight stacks total (four per die), the B200 GPU provides 192 GB of on-package memory, though 180 GB is usable after accounting for ECC and system overhead.</p>
<p>This bandwidth increase is critical. Tensor Core operations can consume data at enormous rates. If HBM can't feed data fast enough, the compute units stall, leaving expensive silicon idle. Blackwell's 8 TB/s keeps the tensor cores fed even during the largest matrix multiplications.</p>
<h3 id="heading-lpddr5x-the-extended-tier">LPDDR5X: The Extended Tier</h3>
<p>Beyond the GPU's HBM sits the Grace CPU's LPDDR5X memory, approximately 480 GB accessible at up to 500 GB/s locally, or ~900 GB/s when accessed from the GPU via NVLink C-2-C.</p>
<p>Accessing LPDDR5X from the GPU has roughly 10× lower bandwidth and higher latency compared to HBM. But it remains far faster than NVMe SSDs or network storage.</p>
<p>LPDDR5X serves as a high-speed overflow tier. Data that doesn't fit in HBM, such as large embedding tables, KV caches for long-context inference, or checkpoint buffers, can reside in CPU memory without catastrophic performance penalties.</p>
<h3 id="heading-data-flow-in-practice">Data Flow in Practice</h3>
<p>When a Blackwell GPU executes an AI workload, data flows through this hierarchy in stages:</p>
<ol>
<li><p><strong>Model loading</strong>: Weights move from storage → CPU memory → HBM (or stay in LPDDR5X if HBM is full)</p>
</li>
<li><p><strong>Batch processing</strong>: Input data streams into HBM, then into L2 as SMs request it</p>
</li>
<li><p><strong>Computation</strong>: Active data moves from L2 → L1/Shared → Registers as operations execute</p>
</li>
<li><p><strong>Output</strong>: Results flow back down the hierarchy to HBM or CPU memory</p>
</li>
</ol>
<p>Each tier serves as a buffer for the tier above it.</p>
<h2 id="heading-practical-example-running-llama-3-70b">Practical Example: Running Llama 3 70B</h2>
<p>Consider deploying Llama 3 70B for inference. In FP16 precision (Note with GB200, you can go as low as FP4), the model weights alone require approximately 140 GB of memory.</p>
<p><strong>On a Hopper H100 (80 GB HBM3):</strong> The model doesn't fit. You must either quantize aggressively, use tensor parallelism across multiple GPUs, or offload layers to CPU memory over PCIe (slow at ~64 GB/s).</p>
<p><strong>On a single GB200 Superchip (~360 GB usable HBM3e + ~480 GB LPDDR5X):</strong> The full 140 GB model fits easily within a single GPU's HBM, leaving the second GPU's HBM and all CPU memory available for KV cache, batching, or running multiple model instances. No model parallelism required. No aggressive quantization forced by memory limits. The GB200 Superchip provides roughly <strong>10× the usable memory</strong> of a single H100, fundamentally changing what fits on one unit</p>
<p>This is the practical impact of Blackwell's memory architecture: models that previously required multi-GPU setups can now run on a single superchip, simplifying deployment and reducing inter-GPU communication overhead.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Memory has always been the limiting factor in AI hardware. Blackwell changes that equation.</p>
<p>By combining dual-die GPUs, HBM3e with 8 TB/s bandwidth, and unified CPU-GPU memory through NVLink C2C, NVIDIA has delivered a system where a single superchip offers roughly 10× the usable memory of its predecessor. Models that once demanded complex multi-GPU orchestration now fit on one unit.</p>
<p>For AI engineers, this means spending less time working around memory constraints and more time building better models. The architecture isn't just faster, it's fundamentally simpler to work with.</p>
<p>As models continue to grow, Blackwell's memory-first design philosophy points to where GPU architecture is heading: tighter integration, unified memory pools, and specialized hardware for the workloads that matter most.</p>
<h3 id="heading-references">References</h3>
<ol>
<li><p>NVIDIA Blackwell Architecture Technical Brief: <a href="https://resources.nvidia.com/en-us-blackwell-architecture">https://resources.nvidia.com/en-us-blackwell-architecture</a></p>
</li>
<li><p>NVIDIA Blackwell Architecture: A Deep Dive: <a href="https://medium.com/@kvnagesh/nvidia-blackwell-architecture-a-deep-dive-into-the-next-generation-of-ai-computing-79c2b1ce3c1b">https://medium.com/@kvnagesh/nvidia-blackwell-architecture-a-deep-dive-into-the-next-generation-of-ai-computing-79c2b1ce3c1b</a></p>
</li>
<li><p>AI Systems Performance Engineering: <a href="https://learning.oreilly.com/library/view/ai-systems-performance/9798341627772/">https://learning.oreilly.com/library/view/ai-systems-performance/9798341627772/</a></p>
</li>
<li><p>Memory Hierarchy of GPUs**:** <a href="https://www.arccompute.io/arc-blog/gpu-101-memory-hierarchy">https://www.arccompute.io/arc-blog/gpu-101-memory-hierarchy</a></p>
</li>
<li><p>GPU Memory Bandwidth and Its Impact on Performance: <a href="https://www.digitalocean.com/community/tutorials/gpu-memory-bandwidth">https://www.digitalocean.com/community/tutorials/gpu-memory-bandwidth</a></p>
</li>
<li><p>The AI Systems Game: <a href="https://medium.com/@adi.fu7/the-ai-systems-game-are-chip-to-chip-interconnects-the-future-of-inference-ec3bbda53eb3">https://medium.com/@adi.fu7/the-ai-systems-game-are-chip-to-chip-interconnects-the-future-of-inference-ec3bbda53eb3</a></p>
</li>
<li><p>CUDA GPU Memory Types: <a href="https://medium.com/@jghaly00/cuda-gpu-memory-types-a07428b3eb16">https://medium.com/@jghaly00/cuda-gpu-memory-types-a07428b3eb16</a></p>
</li>
</ol>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Containerize Your MLOps Pipeline from Training to Serving ]]>
                </title>
                <description>
                    <![CDATA[ Last year, our ML team shipped a fraud detection model that worked perfectly in a Jupyter notebook. Precision was excellent. Recall numbers looked great. Everyone was excited – until we tried to deplo ]]>
                </description>
                <link>https://www.freecodecamp.org/news/containerize-mlops-pipeline-from-training-to-serving/</link>
                <guid isPermaLink="false">69b33f5993256dfc5313bee2</guid>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mlops ]]>
                    </category>
                
                    <category>
                        <![CDATA[ production ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ NVIDIA ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Machine Learning ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Balajee Asish Brahmandam ]]>
                </dc:creator>
                <pubDate>Thu, 12 Mar 2026 22:34:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/156eaca3-8884-4f57-9010-9766278dbf5a.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Last year, our ML team shipped a fraud detection model that worked perfectly in a Jupyter notebook. Precision was excellent. Recall numbers looked great. Everyone was excited – until we tried to deploy it.</p>
<p>The model depended on a specific version of scikit-learn that conflicted with the production Python environment. The feature engineering pipeline required a NumPy build compiled against OpenBLAS, but the deployment servers ran MKL. A preprocessing step used a system library that existed on the data scientist's MacBook but not on the Ubuntu deployment target.</p>
<p>Three weeks of debugging later, we had the model running in production. Three weeks. For a model that was technically finished.</p>
<p>That experience is what pushed me to containerize our entire MLOps pipeline end to end. Not because Docker is trendy in ML circles, but because the alternative (hand-tuning environments, writing installation scripts that break on the next OS update, praying that what worked in training works in production) was costing us more time than the actual model development.</p>
<p>In this tutorial, you'll learn how to structure training and serving containers with multi-stage builds, how to set up experiment tracking with MLflow, how to version your training data with DVC, how to configure GPU passthrough for training, and how to tie it all together into a single Compose file with profiles. This is based on a year of running containerized ML pipelines across three teams.</p>
<h3 id="heading-prerequisites">Prerequisites</h3>
<ul>
<li><p>Docker Engine 24+ or Docker Desktop 4.20+ with Compose v2.22.0+</p>
</li>
<li><p>For GPU training, you'll need the <a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html">NVIDIA Container Toolkit</a> installed on the host and a compatible GPU driver. Run <code>nvidia-smi</code> to verify your GPU is visible, and <code>docker compose version</code> to check your Compose version.</p>
</li>
<li><p>Familiarity with Python, basic Docker concepts, and ML workflows (training, evaluation, serving) is assumed.</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-the-mlops-lifecycle-where-containers-fit">The MLOps Lifecycle: Where Containers Fit</a></p>
</li>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-how-to-build-the-training-container">How to Build the Training Container</a></p>
<ul>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-separate-training-from-serving-requirements">Separate Training from Serving Requirements</a></p>
</li>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-cuda-and-driver-compatibility">CUDA and Driver Compatibility</a></p>
</li>
</ul>
</li>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-how-to-set-up-experiment-tracking-with-mlflow">How to Set Up Experiment Tracking with MLflow</a></p>
</li>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-how-to-version-training-data-with-dvc">How to Version Training Data with DVC</a></p>
</li>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-how-to-build-the-serving-container">How to Build the Serving Container</a></p>
<ul>
<li><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-decouple-models-from-containers">Decouple Models from Containers</a></li>
</ul>
</li>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-how-to-configure-gpu-passthrough-for-training">How to Configure GPU Passthrough for Training</a></p>
</li>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-how-to-tie-it-all-together-with-compose-profiles">How to Tie It All Together with Compose Profiles</a></p>
</li>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-reproducibility-the-whole-point">Reproducibility: The Whole Point</a></p>
</li>
<li><p><a href="https://claude.ai/chat/742d453d-7543-4904-805f-61c5320b4fdb#heading-where-this-breaks-down">Where This Breaks Down</a></p>
</li>
</ul>
<h2 id="heading-the-mlops-lifecycle-where-containers-fit">The MLOps Lifecycle: Where Containers Fit</h2>
<p>If you've built a machine learning model, you know the process has a lot of stages. But if you're coming from a software engineering background (or you're a data scientist who mostly works in notebooks), it helps to see the full picture of what an MLOps pipeline looks like and where Docker fits into each stage.</p>
<p>An MLOps pipeline is a chain of interdependent stages:</p>
<ol>
<li><p><strong>Data ingestion and validation.</strong> Raw data comes in from databases, APIs, or file systems. You clean it, validate it, and store it in a format your model can use.</p>
</li>
<li><p><strong>Feature engineering.</strong> You transform raw data into features the model can learn from. This might be as simple as normalizing numbers or as complex as generating embeddings.</p>
</li>
<li><p><strong>Experiment tracking.</strong> You log every training run's configuration (hyperparameters, data version, code version) and results (accuracy, loss, evaluation metrics) so you can compare experiments and reproduce the best ones.</p>
</li>
<li><p><strong>Model training.</strong> The model learns from your features. This is the compute-heavy part that often needs GPUs.</p>
</li>
<li><p><strong>Evaluation.</strong> You measure the trained model against test data to see if it's good enough to deploy.</p>
</li>
<li><p><strong>Packaging and serving.</strong> You wrap the trained model in an API so other systems can send it data and get predictions back.</p>
</li>
<li><p><strong>Monitoring.</strong> You watch the model in production to catch problems like data drift (when the real-world data starts looking different from the training data) or performance degradation.</p>
</li>
</ol>
<p>Each stage has different computational needs. Training might require GPUs and terabytes of memory. Serving needs low latency and horizontal scaling. Feature engineering might need distributed processing tools like Spark or Dask.</p>
<p>The thing that changed our approach: you don't containerize the entire pipeline as one monolithic image. You containerize each stage independently, with shared interfaces between them.</p>
<p>Think of it like microservices applied to ML infrastructure. Each container does one thing, does it well, and communicates with the others through well-defined interfaces: model artifacts stored in a registry, metrics logged to MLflow, data versioned in object storage.</p>
<p>This gives you the flexibility to:</p>
<ul>
<li><p>Scale training on expensive GPU instances while running serving on cheaper CPU nodes</p>
</li>
<li><p>Update your feature engineering code without rebuilding your training environment</p>
</li>
<li><p>Version each stage independently in your container registry</p>
</li>
<li><p>Let data scientists and ML engineers work on training while platform engineers optimize serving</p>
</li>
</ul>
<h2 id="heading-how-to-build-the-training-container">How to Build the Training Container</h2>
<p>The training container is where most teams start, and where most teams make their first mistake.</p>
<p>The temptation is to create one massive image with every possible library, every CUDA version, every data processing tool. I've seen training images exceed 15GB. They take twenty minutes to build, ten minutes to push, and break whenever someone adds a new dependency.</p>
<p>Here's the pattern that works: use multi-stage builds to separate the build environment from the runtime environment, and use cache mounts to avoid re-downloading packages on every build.</p>
<p>If you're new to these concepts: a <strong>multi-stage build</strong> lets you use one Docker image to build your software and a different, smaller image to run it. You copy only the final artifacts from the build stage to the runtime stage, leaving behind compilers, build tools, and other things you don't need in production.</p>
<p>A <strong>cache mount</strong> tells Docker to keep a directory (like pip's download cache) between builds, so it doesn't re-download packages that haven't changed.</p>
<p>Here's the training Dockerfile:</p>
<pre><code class="language-dockerfile"># syntax=docker/dockerfile:1.4
FROM nvidia/cuda:12.6.3-runtime-ubuntu22.04 AS base

# System dependencies (rarely change)
RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
    python3.11 python3.11-venv python3-pip git curl &amp;&amp; \
    rm -rf /var/lib/apt/lists/*

RUN python3.11 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

# Dependencies (change occasionally)
COPY requirements-train.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements-train.txt

# Training code (changes frequently)
COPY src/ /app/src/
COPY configs/ /app/configs/

WORKDIR /app
ENTRYPOINT ["python", "-m", "src.train"]
</code></pre>
<p>Notice the layer ordering. Docker builds images in layers, and it caches each layer. If a layer hasn't changed, Docker reuses the cached version instead of rebuilding it. But here's the catch: if one layer changes, Docker rebuilds that layer and every layer after it.</p>
<p>That's why we put things in order of how often they change:</p>
<ol>
<li><p><strong>System packages at the top</strong> (they almost never change). Installing <code>python3.11</code> and <code>git</code> takes time, but you only do it once.</p>
</li>
<li><p><strong>Python dependencies in the middle</strong> (they change when you add or update a library). This layer rebuilds when <code>requirements-train.txt</code> changes.</p>
</li>
<li><p><strong>Your actual code at the bottom</strong> (changes on every commit). This is the layer that rebuilds most often.</p>
</li>
</ol>
<p>With this ordering, a code change only rebuilds the final layer, not the entire image. If you put <code>COPY src/</code> before <code>pip install</code>, every code change would trigger a full reinstall of all Python packages. That's the mistake I see most often in ML Dockerfiles.</p>
<p>The <code>--mount=type=cache,target=/root/.cache/pip</code> line on the <code>pip install</code> command tells Docker to persist pip's download cache between builds. When you do update requirements, pip checks the cache first and only downloads packages that are new or changed. On a project with hundreds of ML dependencies (PyTorch alone pulls in dozens of sub-packages), this saves five to ten minutes per build.</p>
<h3 id="heading-separate-training-from-serving-requirements">Separate Training from Serving Requirements</h3>
<p>Your training environment needs libraries that your serving environment does not. Training needs experiment tracking tools like MLflow, data processing libraries like pandas and polars, visualization libraries for debugging, and hyperparameter tuning frameworks. Serving needs a lightweight inference runtime, an API framework like FastAPI, health check endpoints, and minimal overhead.</p>
<p>It's a good idea to maintain separate requirements files:</p>
<pre><code class="language-plaintext"># requirements-train.txt
torch==2.5.1
scikit-learn==1.6.1
mlflow==2.19.0
pandas==2.2.3
polars==1.20.0
dvc[s3]==3.59.1
optuna==4.2.0
matplotlib==3.10.0

# requirements-serve.txt
torch==2.5.1
scikit-learn==1.6.1
mlflow==2.19.0
fastapi==0.115.0
uvicorn[standard]==0.34.0
pydantic==2.10.0
</code></pre>
<p>The overlap is smaller than you'd think. <code>torch</code> and <code>scikit-learn</code> appear in both because the model needs them for inference. Everything else in the training file is baggage that slows down serving deployments and increases the attack surface.</p>
<h3 id="heading-cuda-and-driver-compatibility">CUDA and Driver Compatibility</h3>
<p>One thing that will bite you if you ignore it: the CUDA runtime version inside your container must be compatible with the GPU driver version on the host. The rule is that the host driver must be equal to or newer than the CUDA version in the container. For example, CUDA 12.6 requires driver version 560.28+ on Linux.</p>
<p>Make sure you check your host driver version before choosing your base image:</p>
<pre><code class="language-bash"># On the host machine
nvidia-smi
# Look for "Driver Version: 560.35.03" and "CUDA Version: 12.6"

# The CUDA version shown by nvidia-smi is the maximum CUDA version
# your driver supports, not the version installed
</code></pre>
<p>If your host driver is 535.x, don't use a <code>cuda:12.6</code> base image. Use <code>cuda:12.2</code> or upgrade the driver. Mismatched versions produce cryptic errors like <code>CUDA error: no kernel image is available for execution on the device</code> that are painful to debug.</p>
<p>Pin your base images to specific tags (not <code>latest</code>) and document the minimum driver version in your README. When you deploy to new hardware, the driver version check should be part of your provisioning checklist.</p>
<h2 id="heading-how-to-set-up-experiment-tracking-with-mlflow">How to Set Up Experiment Tracking with MLflow</h2>
<p>If you've ever trained a model and thought "wait, which hyperparameters gave me that good result last week?", you need experiment tracking. Without it, ML development turns into a mess of Jupyter notebooks, screenshots of metrics, and spreadsheets that nobody keeps up to date.</p>
<p><a href="https://mlflow.org/">MLflow</a> is the most widely adopted open-source tool for this. It logs three things for every training run: <strong>parameters</strong> (learning rate, batch size, number of epochs), <strong>metrics</strong> (accuracy, loss, F1 score), and <strong>artifacts</strong> (the trained model file, plots, evaluation reports). It stores all of this in a database and gives you a web UI to compare runs side by side.</p>
<p>Running MLflow as a containerized service means the tracking server is persistent and shared across your team, not running on one person's laptop:</p>
<pre><code class="language-yaml">services:
  mlflow:
    image: ghcr.io/mlflow/mlflow:v2.19.0
    command: &gt;
      mlflow server
      --backend-store-uri postgresql://mlflow:secret@db/mlflow
      --default-artifact-root /mlflow/artifacts
      --host 0.0.0.0
    ports:
      - "5000:5000"
    volumes:
      - mlflow-artifacts:/mlflow/artifacts
    depends_on:
      db: { condition: service_healthy }

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mlflow
      POSTGRES_USER: mlflow
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U mlflow"]
      interval: 5s
      timeout: 2s
      retries: 5
      start_period: 10s
    volumes:
      - postgres-data:/var/lib/postgresql/data

volumes:
  mlflow-artifacts:
  postgres-data:
</code></pre>
<p>Let me break down what's happening here.</p>
<p>The <code>mlflow</code> service runs the MLflow tracking server. It stores experiment metadata (parameters, metrics) in a Postgres database and saves artifacts (model files, plots) to a Docker volume.</p>
<p>The <code>depends_on</code> with <code>condition: service_healthy</code> tells Compose to wait until Postgres is actually ready to accept connections before starting MLflow. Without this, MLflow would crash on startup because the database isn't ready yet.</p>
<p>The <code>db</code> service runs Postgres with a health check that uses <code>pg_isready</code>, a built-in Postgres utility that checks if the database is accepting connections. The <code>start_period</code> gives Postgres 10 seconds to initialize before health checks start counting failures.</p>
<p>Your training code connects to MLflow by setting one environment variable:</p>
<pre><code class="language-python">import os
import mlflow

# This tells MLflow where to log experiments
# When running inside Docker Compose, "mlflow" resolves to the mlflow container
os.environ["MLFLOW_TRACKING_URI"] = "http://mlflow:5000"

# Example: logging a training run
with mlflow.start_run(run_name="fraud-detector-v2"):
    # Log hyperparameters
    mlflow.log_param("learning_rate", 0.001)
    mlflow.log_param("batch_size", 64)
    mlflow.log_param("epochs", 50)

    # ... train your model here ...

    # Log metrics
    mlflow.log_metric("accuracy", 0.94)
    mlflow.log_metric("f1_score", 0.91)
    mlflow.log_metric("precision", 0.93)
    mlflow.log_metric("recall", 0.89)

    # Log the trained model as an artifact
    mlflow.sklearn.log_model(model, "model")
    # Or for PyTorch: mlflow.pytorch.log_model(model, "model")
</code></pre>
<p>After the run completes, open <code>http://localhost:5000</code> in your browser. You'll see a table of all your runs with their parameters and metrics. Click any run to see details, compare it with other runs, or download the model artifact. No more "I think experiment 7 was the good one" conversations.</p>
<p>A note on the password in the YAML: for local development this is fine. For staging and production, use Docker secrets or inject the credentials from your CI environment. Don't commit real database passwords to your repo.</p>
<h2 id="heading-how-to-version-training-data-with-dvc">How to Version Training Data with DVC</h2>
<p>Models are reproducible only if you can also reproduce the data they were trained on. This is a problem Git can't solve on its own, because training datasets are often gigabytes or terabytes in size and Git isn't designed for large binary files.</p>
<p><a href="https://dvc.org/">DVC (Data Version Control)</a> fills this gap. It works like Git, but for data. Here's the concept: instead of storing your 10GB training dataset in Git, DVC stores a small text file (a <code>.dvc</code> file) that acts as a pointer to the actual data. The real data lives in cloud storage (S3, Google Cloud Storage, Azure Blob). When you check out a specific Git commit, DVC knows which version of the data goes with that commit and can pull it from remote storage.</p>
<p>The workflow on your local machine looks like this:</p>
<pre><code class="language-bash"># Initialize DVC in your project (one time)
dvc init

# Add your training data to DVC tracking
dvc add data/training_data.parquet
# This creates data/training_data.parquet.dvc (small pointer file)
# and adds training_data.parquet to .gitignore

# Push the actual data to remote storage
dvc push

# Commit the pointer file to Git
git add data/training_data.parquet.dvc .gitignore
git commit -m "Add training data v1"
</code></pre>
<p>Now your Git repo contains the pointer file, and the real data lives in S3. When someone else (or a container) needs the data, they run <code>dvc pull</code> and DVC downloads it from remote storage.</p>
<p>The training Dockerfile includes DVC, and the entrypoint pulls the correct data version before training begins:</p>
<pre><code class="language-dockerfile"># syntax=docker/dockerfile:1.4
FROM nvidia/cuda:12.6.3-runtime-ubuntu22.04 AS base

RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends \
    python3.11 python3.11-venv python3-pip git curl &amp;&amp; \
    rm -rf /var/lib/apt/lists/*

RUN python3.11 -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements-train.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements-train.txt

COPY src/ /app/src/
COPY configs/ /app/configs/

# DVC tracking files (these are small text files in Git)
COPY data/*.dvc /app/data/
COPY .dvc/ /app/.dvc/

WORKDIR /app
COPY entrypoint.sh .
RUN chmod +x entrypoint.sh
ENTRYPOINT ["./entrypoint.sh"]
</code></pre>
<p>The entrypoint script pulls the data and then starts training:</p>
<pre><code class="language-bash">#!/bin/bash
set -e

echo "Pulling training data from remote storage..."
dvc pull data/

echo "Starting training run..."
python -m src.train "$@"
</code></pre>
<p>For DVC to pull from S3, the container needs AWS credentials. You can pass them as environment variables in your Compose file or mount them from the host:</p>
<pre><code class="language-yaml">training:
  build: { context: ., dockerfile: Dockerfile.train }
  environment:
    - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
    - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
    - AWS_DEFAULT_REGION=us-east-1
</code></pre>
<p>Combined with MLflow's experiment logging, you get a complete provenance chain: this model was trained on this version of the data (tracked by DVC), with these parameters (logged in MLflow), producing these metrics.</p>
<p>You can reproduce any past experiment by checking out the Git commit and running the training container.</p>
<h2 id="heading-how-to-build-the-serving-container">How to Build the Serving Container</h2>
<p>"Serving" means wrapping your trained model in an API so other systems can send it data and get predictions back. For example, a fraud detection model might expose a <code>/predict</code> endpoint that accepts transaction data and returns a fraud probability.</p>
<p>The serving container has different priorities than the training container. Training optimizes for flexibility and raw compute. Serving optimizes for speed, small size, and reliability:</p>
<pre><code class="language-dockerfile">FROM python:3.11-slim AS serving

WORKDIR /app

# Install curl for healthcheck
RUN apt-get update &amp;&amp; apt-get install -y --no-install-recommends curl &amp;&amp; \
    rm -rf /var/lib/apt/lists/*

COPY requirements-serve.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements-serve.txt

COPY src/serving/ /app/src/serving/

HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

EXPOSE 8000
CMD ["uvicorn", "src.serving.app:app", "--host", "0.0.0.0"]
</code></pre>
<p>A few things to understand if you're new to this:</p>
<p><code>uvicorn</code> is a lightweight Python web server that runs <a href="https://fastapi.tiangolo.com/">FastAPI</a> applications. FastAPI is a framework for building APIs in Python. Together, they let you turn your model into a web service that responds to HTTP requests.</p>
<p><code>HEALTHCHECK</code> tells Docker to periodically check if your container is actually working, not just running. Every 30 seconds, Docker runs the <code>curl</code> command against the <code>/health</code> endpoint. If it fails three times in a row, Docker marks the container as unhealthy. This matters because your model server might be running but not ready (maybe the model file is still downloading), and you don't want to send traffic to a server that can't respond.</p>
<p><code>start-period</code> of 60 seconds is important for ML serving containers. Model loading can take time, especially for large models (loading a 2GB model from a registry takes a while). Without <code>start-period</code>, the health check would start failing immediately, count those failures toward the retry limit, and the orchestrator might kill the container before the model finishes loading. The start period gives the container grace time to initialize.</p>
<p>Notice we're using <code>python:3.11-slim</code> here, not the NVIDIA CUDA image. Most trained models can run inference on CPU. If you need GPU inference (for example, running a large language model or doing real-time video processing), use the CUDA base image instead, but be aware that it makes the serving container much larger.</p>
<p>If you want to skip the <code>curl</code> dependency, use Python's built-in <code>urllib</code> for the health check:</p>
<pre><code class="language-dockerfile">HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=3 \
    CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
</code></pre>
<h3 id="heading-decouple-models-from-containers">Decouple Models from Containers</h3>
<p>This is one of the most important patterns in this article, and the one beginners most often get wrong.</p>
<p>The temptation is to copy your trained model file (the <code>.pkl</code>, <code>.pt</code>, or <code>.onnx</code> file that contains the learned weights) directly into the Docker image during the build. Don't do this. When you embed model files in your Docker image, every model update requires a new image build and push. For a 2GB model, that means rebuilding the container, uploading 2GB to a registry, and redeploying, even though only the model changed and the code is identical.</p>
<p>Instead, have your serving container download the model from a model registry (like MLflow) or cloud storage (like S3) at startup. The container image stays small and generic. Model updates become a configuration change (pointing to a new model version) rather than a deployment.</p>
<p>Here's a full serving app using FastAPI with the modern lifespan pattern. If you've used Flask, FastAPI is similar but faster and with built-in request validation:</p>
<pre><code class="language-python">import os
from contextlib import asynccontextmanager

import mlflow
from fastapi import FastAPI

# MODEL_URI points to a specific model version in MLflow's registry
# Format: "models:/&lt;model-name&gt;/&lt;stage&gt;" where stage is Staging or Production
MODEL_URI = os.environ.get("MODEL_URI", "models:/fraud-detector/production")
model = None


@asynccontextmanager
async def lifespan(app: FastAPI):
    # This runs once when the server starts up
    global model
    print(f"Loading model from {MODEL_URI}...")
    model = mlflow.pyfunc.load_model(MODEL_URI)
    print("Model loaded successfully.")
    yield
    # This runs when the server shuts down
    print("Shutting down model server.")


app = FastAPI(lifespan=lifespan)


@app.get("/health")
async def health():
    """Used by Docker HEALTHCHECK to verify the server is ready."""
    if model is None:
        return {"status": "loading"}, 503
    return {"status": "healthy"}


@app.post("/predict")
async def predict(features: dict):
    """Accept features as JSON, return model prediction."""
    import pandas as pd

    # Convert the input dict into a DataFrame (what most sklearn/mlflow models expect)
    df = pd.DataFrame([features])
    prediction = model.predict(df)
    return {"prediction": prediction.tolist()}
</code></pre>
<p>When a client sends a POST request to <code>/predict</code> with JSON like <code>{"amount": 500, "merchant_category": "electronics", "hour": 23}</code>, the model returns a prediction. The <code>/health</code> endpoint returns 503 while the model is loading and 200 once it's ready, which is exactly what the Docker <code>HEALTHCHECK</code> checks for.</p>
<p>Promoting a new model version means updating the <code>MODEL_URI</code> environment variable and restarting the container. The MLflow model registry supports stage transitions (Staging, Production, Archived), so you can promote a model in the MLflow UI and then point your serving container at the new version.</p>
<p>For zero-downtime model updates, implement a reload endpoint that swaps models without restarting:</p>
<pre><code class="language-python">@app.post("/admin/reload")
async def reload_model():
    global model
    model = mlflow.pyfunc.load_model(MODEL_URI)
    return {"status": "reloaded"}
</code></pre>
<h2 id="heading-how-to-configure-gpu-passthrough-for-training">How to Configure GPU Passthrough for Training</h2>
<p>By default, Docker containers can't see the GPU hardware on the host machine. "GPU passthrough" means giving a container access to the host's GPUs so that libraries like PyTorch and TensorFlow can use them for accelerated computation.</p>
<p>This requires two things on the host (the machine running Docker, not inside the container):</p>
<ol>
<li><p><strong>NVIDIA GPU drivers</strong> installed and working. Verify with <code>nvidia-smi</code>. If that command shows your GPUs, you're good.</p>
</li>
<li><p><strong>NVIDIA Container Toolkit</strong> installed. This is the bridge between Docker and the GPU drivers. Install it from the <a href="https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html">NVIDIA docs</a> and verify with <code>docker run --rm --gpus all nvidia/cuda:12.6.3-base-ubuntu22.04 nvidia-smi</code>. If you see your GPU listed, the toolkit is working.</p>
</li>
</ol>
<p>Once the host is set up, GPU access in Docker Compose looks like this:</p>
<pre><code class="language-yaml">services:
  training:
    build: { context: ., dockerfile: Dockerfile.train }
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    volumes:
      - ./data:/app/data
    environment:
      - MLFLOW_TRACKING_URI=http://mlflow:5000
</code></pre>
<p>The <code>deploy.resources.reservations.devices</code> block is saying: "this container needs all available NVIDIA GPUs." Inside the container, PyTorch and TensorFlow will see the GPUs and use them automatically. You can verify by adding <code>print(torch.cuda.is_available())</code> to your training script, which should print <code>True</code>.</p>
<p>If you're running Compose v2.30.0+, you can use the shorter <code>gpus</code> syntax:</p>
<pre><code class="language-yaml">services:
  training:
    build: { context: ., dockerfile: Dockerfile.train }
    gpus: all
    volumes:
      - ./data:/app/data
    environment:
      - MLFLOW_TRACKING_URI=http://mlflow:5000
</code></pre>
<p>For multi-GPU training with frameworks like PyTorch's DistributedDataParallel, you can assign specific GPUs using <code>device_ids</code>. This matters when running multiple training jobs at the same time:</p>
<pre><code class="language-yaml">services:
  training-job-1:
    build: { context: ., dockerfile: Dockerfile.train }
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ["0", "1"]
              capabilities: [gpu]
    environment:
      - CUDA_VISIBLE_DEVICES=0,1

  training-job-2:
    build: { context: ., dockerfile: Dockerfile.train }
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              device_ids: ["2", "3"]
              capabilities: [gpu]
    environment:
      - CUDA_VISIBLE_DEVICES=0,1
</code></pre>
<p>Note that <code>CUDA_VISIBLE_DEVICES</code> inside the container is relative to the devices assigned by Docker, not the host GPU indices. Both containers see their GPUs as device 0 and 1, even though they're using different physical GPUs.</p>
<h2 id="heading-how-to-tie-it-all-together-with-compose-profiles">How to Tie It All Together with Compose Profiles</h2>
<p>If you're new to Compose profiles: by default, <code>docker compose up</code> starts every service defined in your <code>docker-compose.yml</code>. But you don't always want everything running. Your MLflow server and serving API should run all the time, but the training container should only launch when you're actually training a model (and it needs a GPU, which you might not have on your laptop).</p>
<p>Profiles solve this. When you add <code>profiles: ["train"]</code> to a service, that service is excluded from <code>docker compose up</code> by default. It only starts when you explicitly activate the profile with <code>docker compose --profile train</code>. This means one file defines your entire ML infrastructure, but you control what runs and when.</p>
<p>Here's the complete <code>docker-compose.yml</code> that ties every piece from this article together:</p>
<pre><code class="language-yaml">services:
  # --- Always-on infrastructure ---
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: mlflow
      POSTGRES_USER: mlflow
      POSTGRES_PASSWORD: secret
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U mlflow"]
      interval: 5s
      timeout: 2s
      retries: 5
      start_period: 10s
    volumes:
      - postgres-data:/var/lib/postgresql/data

  mlflow:
    image: ghcr.io/mlflow/mlflow:v2.19.0
    command: &gt;
      mlflow server
      --backend-store-uri postgresql://mlflow:secret@db/mlflow
      --default-artifact-root /mlflow/artifacts
      --host 0.0.0.0
    ports:
      - "5000:5000"
    volumes:
      - mlflow-artifacts:/mlflow/artifacts
    depends_on:
      db: { condition: service_healthy }

  serving:
    build: { context: ., dockerfile: Dockerfile.serve }
    ports:
      - "8000:8000"
    environment:
      - MODEL_URI=models:/fraud-detector/production
      - MLFLOW_TRACKING_URI=http://mlflow:5000
    depends_on:
      mlflow: { condition: service_started }

  # --- Training (on-demand) ---
  training:
    build: { context: ., dockerfile: Dockerfile.train }
    profiles: ["train"]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]
    volumes:
      - ./data:/app/data
      - ./configs:/app/configs
    environment:
      - MLFLOW_TRACKING_URI=http://mlflow:5000
      - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
      - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
    depends_on:
      mlflow: { condition: service_started }

volumes:
  postgres-data:
  mlflow-artifacts:
</code></pre>
<p>The day-to-day workflow with this file:</p>
<pre><code class="language-bash"># Step 1: Start the infrastructure (MLflow + Postgres + serving API)
# The -d flag runs everything in the background
docker compose up -d

# Step 2: Open the MLflow UI to see past experiments
open http://localhost:5000    # macOS
# xdg-open http://localhost:5000  # Linux

# Step 3: Check that the serving API is healthy
curl http://localhost:8000/health
# Should return: {"status":"healthy"}

# Step 4: Run a training job (pulls data via DVC, logs to MLflow)
# This only starts the "training" service because of the profile flag
docker compose --profile train run training

# Step 5: Watch training progress in the MLflow UI at localhost:5000
# You'll see metrics updating in real time if your training code logs them

# Step 6: After training completes, promote the model in MLflow UI
# Click the model, go to "Register Model", set stage to "Production"

# Step 7: Restart the serving container to pick up the new model version
docker compose restart serving

# Step 8: Test the new model
curl -X POST http://localhost:8000/predict \
  -H "Content-Type: application/json" \
  -d '{"amount": 500, "merchant_category": "electronics", "hour": 23}'
</code></pre>
<p>This single-file approach means a new team member can clone the repo, run <code>docker compose up -d</code>, and have the complete ML infrastructure running locally within minutes. The same containers deploy to staging and production with only environment variable changes (database credentials, model URIs, GPU allocation).</p>
<h2 id="heading-reproducibility-the-whole-point">Reproducibility: The Whole Point</h2>
<p>Everything in this article serves one goal: reproducibility. The ability to take any commit hash, build the same containers, pull the same data, and produce the same model.</p>
<p>Here are the practices that make this work:</p>
<h3 id="heading-pin-everything">Pin Everything</h3>
<p>Pin your base images to specific digests, not just tags. Pin your Python packages to exact versions with <code>pip freeze &gt; requirements.txt</code>. Use fixed random seeds in your training code and log them in MLflow.</p>
<h3 id="heading-log-everything">Log Everything</h3>
<p>Every training run should log the exact library versions (<code>pip freeze</code>), the Git commit hash, the DVC data version, all hyperparameters, and all evaluation metrics to MLflow. You can automate this:</p>
<pre><code class="language-python">import subprocess
import mlflow

with mlflow.start_run():
    # Log environment info automatically
    pip_freeze = subprocess.check_output(["pip", "freeze"]).decode()
    mlflow.log_text(pip_freeze, "pip_freeze.txt")

    git_hash = subprocess.check_output(["git", "rev-parse", "HEAD"]).decode().strip()
    mlflow.log_param("git_commit", git_hash)

    # ... rest of training ...
</code></pre>
<h3 id="heading-version-everything">Version Everything</h3>
<p>Git for code, DVC for data, MLflow for experiments, Docker digests for environments. The combination creates a complete provenance chain. When a stakeholder asks why a model made a particular prediction, you can trace it back to the exact code, data, and hyperparameters that produced it. For regulated industries like finance and healthcare, that traceability is a compliance requirement, not a nice-to-have.</p>
<h2 id="heading-where-this-breaks-down">Where This Breaks Down</h2>
<p>This approach works well for small-to-medium teams running on single hosts or small clusters. Here's where you'll hit walls:</p>
<p><strong>Large datasets.</strong> Don't mount multi-terabyte datasets into containers. Use object storage (S3, GCS) and stream data during training. DVC handles the versioning, but the data itself should live outside Docker entirely.</p>
<p><strong>GPU driver mismatches.</strong> Your container's CUDA version must be compatible with the host driver. Test on identical hardware and driver versions to what you'll run in production. Document the minimum driver version in your README.</p>
<p><strong>Multi-node training.</strong> When you need to distribute training across multiple machines, you'll outgrow Compose. Kubernetes with Kubeflow or KServe is the standard path for distributed training and auto-scaled serving.</p>
<p><strong>Serving at scale.</strong> A single container running uvicorn handles moderate traffic. For high-throughput inference, you'll need a load balancer, multiple replicas, and potentially a dedicated serving framework like NVIDIA Triton Inference Server or TensorFlow Serving. Compose can run multiple replicas with <code>docker compose up --scale serving=3</code>, but it doesn't give you the routing, health-based load balancing, or rolling updates that a real orchestrator provides.</p>
<p><strong>Secrets in production.</strong> The Compose file above uses plaintext passwords for local development. In production, use Docker secrets, HashiCorp Vault, or your cloud provider's secret manager. Never commit credentials to your repo.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Containerizing your MLOps pipeline turns fragile, environment-dependent models into reproducible, deployable artifacts. Multi-stage builds keep images lean. MLflow gives you experiment tracking and model lineage. DVC links code to data. GPU passthrough preserves training performance. A single Compose file with profiles ties the whole workflow together.</p>
<p>That fraud detection model I mentioned at the start? We eventually containerized the entire pipeline around it. The next model we shipped went from "notebook finished" to "running in production" in two days instead of three weeks. Most of that time was spent on evaluation and review, not fighting environments.</p>
<p>Containerization doesn't make your models better. It gets the infrastructure out of the way so you can focus on the work that does.</p>
<p>But even with these caveats, containerized MLOps eliminates the most common source of ML project delays: environment mismatch between development and production. The three weeks we spent debugging that fraud detection model deployment? That doesn't happen anymore.</p>
<p>If you found this useful, you can find me writing about MLOps, containerized workflows, and production AI systems on my blog.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
