<?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" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Databricksters: AI & ML]]></title><description><![CDATA[Simplifying Gen AI on Databricks]]></description><link>https://www.databricksters.com/s/gen-ai</link><image><url>https://substackcdn.com/image/fetch/$s_!zPJJ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff49ecae-7c56-403c-9389-61b28de6a50f_1280x1280.png</url><title>Databricksters: AI &amp; ML</title><link>https://www.databricksters.com/s/gen-ai</link></image><generator>Substack</generator><lastBuildDate>Mon, 04 May 2026 18:59:22 GMT</lastBuildDate><atom:link href="https://www.databricksters.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Soni]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[databricksters@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[databricksters@substack.com]]></itunes:email><itunes:name><![CDATA[Canadian Data Guy]]></itunes:name></itunes:owner><itunes:author><![CDATA[Canadian Data Guy]]></itunes:author><googleplay:owner><![CDATA[databricksters@substack.com]]></googleplay:owner><googleplay:email><![CDATA[databricksters@substack.com]]></googleplay:email><googleplay:author><![CDATA[Canadian Data Guy]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Cutting Token Costs Reaches the Renaissance]]></title><description><![CDATA[A Lakebase Powered Solution for Enforcing Token Budgets, Now with Fewer Sharp Edges]]></description><link>https://www.databricksters.com/p/cutting-token-costs-reaches-the-renaissance</link><guid isPermaLink="false">https://www.databricksters.com/p/cutting-token-costs-reaches-the-renaissance</guid><dc:creator><![CDATA[Austin]]></dc:creator><pubDate>Tue, 17 Mar 2026 14:02:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!7sNy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Back in October I published a blog called <a href="https://www.databricksters.com/p/getting-medieval-on-token-costs">Getting Medieval on Token Costs</a>. The code and strategy I provided worked, but as the title implied it was rough around the edges. How rough? Well&#8230;</p><ul><li><p>The API calls to the FMs were synchronous, so QPS would have been Medieval indeed</p></li><li><p>The Lakebase instance was provisioned, so it would always be accruing costs even without usage</p></li><li><p>There was no UI, so you or your admin would be spending hours fiddling with thousand line SQL queries for enterprise use cases</p></li></ul><p>But no matter, we&#8217;ve had a Renaissance!</p><p><a href="https://github.com/azaccor/token-rate-limiter">The repo</a> got three meaningful updates and a handful of smaller ones that collectively move this from being technically functional to something a medium enterprise team might actually want to use on Databricks.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7sNy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7sNy!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg 424w, https://substackcdn.com/image/fetch/$s_!7sNy!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg 848w, https://substackcdn.com/image/fetch/$s_!7sNy!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!7sNy!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7sNy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg" width="1456" height="1532" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1532,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:11842916,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/190565336?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7sNy!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg 424w, https://substackcdn.com/image/fetch/$s_!7sNy!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg 848w, https://substackcdn.com/image/fetch/$s_!7sNy!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!7sNy!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F13774bb4-e2c9-4cf2-a151-17fe03cd9b73_5786x6090.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The Money Changer and His Wife - Quentin Matsys, 1514 oil-on-panel</figcaption></figure></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.databricksters.com/subscribe?"><span>Subscribe now</span></a></p><h3><strong>Quick</strong> <strong>Refresher</strong></h3><p>The original solution uses a custom MLflow model serving endpoint as a proxy between your users and whatever foundation model they&#8217;re calling. Before the request hits the FM, the endpoint checks two Lakebase tables: one for the user&#8217;s token limit and another for how many tokens they&#8217;ve already burned through. If they&#8217;re over budget, the request ends up like John the Baptist in the cover image. If not, it goes through to the FM and the usage is written back to Lakebase along with the response and remaining balance.</p><h3><strong>Change</strong> <strong>1:</strong> <strong>Autoscaling</strong> <strong>Lakebase</strong></h3><p>The original code used a Provisioned Lakebase instance because that was the only option at the time, but it&#8217;s going away and we have something better. Autoscaling Lakebase. </p><p>Swapping to Autoscaling Lakebase means you can set minimum and maximum scaling bands, and if you don&#8217;t need a high availability instance, it will also allow you to scale to zero during times of no use. </p><p>This is the smallest change architecturally, but it&#8217;s nice not to pay for compute we don&#8217;t need.</p><h3><strong>Change</strong> <strong>2:</strong> <strong>ResponsesAgent</strong> <strong>+</strong> <strong>Async</strong> <strong>FM</strong> <strong>Calls</strong></h3><p>The original code used <code>mlflow.pyfunc.PythonModel</code> and called the FM endpoint via <code>requests.post()</code>, which is synchronous and blocking. Only one request can be handled at a time per unit of concurrency. Which meant the endpoint that was supposed to help you manage costs via budgeting is throttling your throughput instead. While I suppose that is one way to reduce token costs, it&#8217;s not very useful.</p><p>The new version replaces the PythonModel with a ResponsesAgent and swaps <code>requests</code> for <code>httpx.AsyncClient</code> inside an <code>async def predict_stream()</code>. Now multiple FM calls can be in flight simultaneously and the serving endpoint isn&#8217;t waiting on one user&#8217;s 20-second Claude response before it can look at the next request in the queue.</p><p>The core logic now lives in a standalone <code>rate_limiter_agent.py</code> decoupled from the notebook. The public API is much cleaner:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2ec01639-67a8-440b-bc77-590a75edefd6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">  agent = TokenRateLimiterAgent(
      db_config={...},
      workspace_client=WorkspaceClient(),
      endpoint_name="ep-your-endpoint",
      group_members={
          "data-science-team": ["andrea@company.com", "john@company.com"],
      },
  )

  # Before calling the FM:
  quota = agent.check_quota("andrea@company.com", "databricks-claude-sonnet-4-5")
  if not quota["allowed"]:
      # Return 429 or block the request
      ...

  # After the FM call completes:
  agent.log_usage(
      user_name="andrea@company.com",
      model_name="databricks-claude-sonnet-4-5",
      prompt_tokens=1200,
      completion_tokens=350,
      request_id="req-abc123",
  )
</code></pre></div><p>You can drop this into any existing pipeline without touching the notebook, which certainly helps if you&#8217;re integrating this into something that already has its own serving infrastructure.</p><h3><strong>Change</strong> <strong>3:</strong> <strong>An</strong> <strong>Actual</strong> <strong>Frontend</strong></h3><p>The original had no management UI, which meant you had to set limits by writing manual SQL queries for any new change to your budgeting policy. Not very convenient. </p><p>The new repo ships a full Databricks App: a React + FastAPI application that deploys alongside your serving endpoint and gives administrators a no-code interface for setting granular budgets. How granular you ask? Any combination of:</p><ul><li><p>A user, service principal, or group</p></li><li><p>Calling any FM, list of FMs, or across all FMs in the workspace</p></li><li><p>That resets every X hours, days, weeks, months, or never</p></li><li><p>Limited to a specified count of tokens or dollars</p><ul><li><p>Pre-populates token costs from Databricks documentation, but manually editable in case this changes or you have some kind of secret discount I don&#8217;t know about</p></li><li><p>This is another nice quality of life feature since tokens are not all created equally; GPT OSS 20B tokens cost about 100x less than GPT 5.4 tokens</p></li></ul></li></ul><p>The drop-downs auto-populate users, SPs, and groups as well as the Databricks Foundation Models.</p><p>It also comes with a handy monitoring dashboard so you can see usage over time, your top consumers, and the most popular models.</p><p>The App authenticates to Lakebase via a native Postgres role with a static password stored in Databricks Secrets, so there&#8217;s no OAuth token refresh to manage.</p><h3><strong>An</strong> <strong>Honest</strong> <strong>Conclusion</strong></h3><p>Is this production-grade for an org running thousands of concurrent end users? Maybe not. You might consider mini-batching requests at scale, but there will still be some amount of cost tracking overhead, and this gets more difficult at scale.</p><p>Is this production-grade for most actual enterprise teams who want to stop their power users from accidentally burning through their monthly token budget in a week? Yes. I think this solution really shines when you have dozens to hundreds of daily active users who might get greedy on Opus requests without some budget enforcement. </p><p>But don&#8217;t take my word for it; check it out for yourself. The code <a href="https://github.com/azaccor/token-rate-limiter">lives here</a> and setup instructions are in the README.</p><p>Cheers and happy coding.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Observability for Any Agent, Anywhere: Production-Ready Tracing with MLflow & OpenTelemetry on Databricks]]></title><description><![CDATA[MLflow OpenTelemetry traces in Unity Catalog create a continuous improvement flywheel for AI agents through analytics, evals, and monitoring.]]></description><link>https://www.databricksters.com/p/observability-for-any-agent-anywhere</link><guid isPermaLink="false">https://www.databricksters.com/p/observability-for-any-agent-anywhere</guid><dc:creator><![CDATA[Anoop Sunke]]></dc:creator><pubDate>Fri, 20 Feb 2026 16:03:07 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d936aaad-6c40-4be8-8c7a-0e4ae848188d_1376x768.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Executive Summary</h2><ul><li><p><strong>The Problem:</strong> AI agents generate massive volumes of trace data, but traditional observability tools make that data expensive to retain, difficult to govern, and hard to use in evaluation and analytics workflows.</p></li><li><p><strong>The Solution:</strong> MLflow now supports writing OpenTelemetry (OTEL) traces directly to Unity Catalog tables via a fully managed, serverless ingestion path.</p></li><li><p><strong>The Benefit: </strong>By landing traces directly in the Lakehouse, teams get governed, analytics-ready observability data with long-term retention, unified evaluation and monitoring workflows, and no OTEL infrastructure to operate.</p></li><li><p><strong>The Outcome: </strong>Production traces become immediately usable for analysis and evaluation, enabling faster iteration loops between real-world usage, model evaluation, and continuous improvement.</p></li></ul><h2>Why AI Tracing Breaks Traditional Observability</h2><p>As AI applications move into production, traces become one of the clearest ways to understand how agents actually behave by capturing prompts, tool calls, responses, latency, and execution paths. Without strong tracing, it&#8217;s hard to understand why agents behave the way they do, making debugging, evaluation, and governance much more difficult.</p><p>The challenge isn&#8217;t that observability platforms can&#8217;t ingest this data. It&#8217;s that AI traces quickly become valuable beyond debugging. Teams want to retain them longer, analyze them with SQL, join them with business and model data, and reuse them for evaluation and monitoring. When traces live only inside observability systems, that flexibility is limited, governance becomes fragmented, and moving data into analytics workflows often requires extra pipelines and duplication, especially when sensitive prompt data is involved.</p><h2>MLflow and OTEL Trace Ingestion</h2><p>Databricks now <a href="https://docs.databricks.com/aws/en/mlflow3/genai/tracing/trace-unity-catalog">supports</a> writing MLflow traces directly to Unity Catalog using the OpenTelemetry (OTEL) format. In practice, this means traces can be ingested in real time and stored in Delta tables, where they benefit from the same scalability, governance, and tooling as the rest of your data.</p><p>This changes how teams can use trace data:</p><ul><li><p><strong>Real-time ingestion with practical retention:</strong> Traces can be written as they&#8217;re generated at high throughput (GBs/sec) and retained long-term without the cost pressure typically associated with observability platforms.</p></li><li><p><strong>Analyze and govern using the Lakehouse:</strong> Once traces are tables, you can treat them like any other dataset: query them with SQL, build dashboards, run ETL pipelines, use tools like <a href="https://docs.databricks.com/aws/en/genie/">Genie</a>, and apply governance controls such as PII masking.</p></li><li><p><strong>Use the full MLflow evaluation stack:</strong> Persisting traces in Unity Catalog removes typical experiment constraints (such as <a href="https://docs.databricks.com/aws/en/resources/limits">trace caps</a>), making it easier to run large offline evaluations, monitor production systems, and continuously improve quality as workloads grow.</p></li></ul><h3>The Engineering trade-off: SaaS vs. Lakehouse</h3><p>So why not rely entirely on a SaaS observability tool?</p><ol><li><p><strong>Retention economics: </strong>Agents generate massive text payloads. Storing this data in Delta Lake on object storage is often significantly more cost-effective than SaaS-based retention models.</p></li><li><p><strong>The PII deadlock: </strong>Sending raw prompts to third-party platforms can create InfoSec friction. Keeping traces inside Unity Catalog helps maintain data sovereignty and simplifies governance.</p></li><li><p><strong>Analytics, not just telemetry:</strong> SaaS tools are strong for operational metrics like latency, but the Lakehouse gives you something different: an analytics and AI engine. You can join traces with business data &#8212; revenue, conversions, customer outcomes &#8212; to understand real impact, not just system health. Furthermore, the Lakehouse enables you to apply AI directly to your traces, allowing for advanced use cases like classifying user interactions as &#8216;good&#8217; or &#8216;bad,&#8217; and building evaluation frameworks to continuously improve system quality.</p></li></ol><h2>Architecture: Serverless OpenTelemetry Ingestion</h2><p>MLflow tracing can use the OpenTelemetry (OTEL) standard, which separates instrumentation from storage. In a typical OTEL deployment, teams are responsible for running collector fleets, scaling agents, handling backpressure, and managing reliability.</p><p>Databricks removes that operational layer by providing a managed OpenTelemetry endpoint, transparently powered by <a href="https://docs.databricks.com/aws/en/ingestion/zerobus-overview">Zerobus</a>. Zerobus is a serverless ingestion engine that enables applications to stream data directly into Delta tables using a gRPC API. Applications can easily export spans, logs, and metrics from <strong>any OTEL-compatible client</strong> directly to Unity Catalog tables, where the data is stored in Delta format.  Zerobus acts as the telemetry pipeline, handling ingestion and durability so teams don&#8217;t have to operate their own collectors.</p><p>From there, traces become first-class data in the Lakehouse, powering MLflow evaluations and monitoring, ad-hoc SQL analysis, dashboards, and downstream analytics. This creates a continuous improvement <strong>flywheel</strong> where production behavior feeds evaluation and analysis, which in turn drives faster iteration and better agent performance.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AIlN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AIlN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png 424w, https://substackcdn.com/image/fetch/$s_!AIlN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png 848w, https://substackcdn.com/image/fetch/$s_!AIlN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png 1272w, https://substackcdn.com/image/fetch/$s_!AIlN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AIlN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png" width="1456" height="777" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:777,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:3766120,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/188328490?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AIlN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png 424w, https://substackcdn.com/image/fetch/$s_!AIlN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png 848w, https://substackcdn.com/image/fetch/$s_!AIlN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png 1272w, https://substackcdn.com/image/fetch/$s_!AIlN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feba10508-cf89-4511-8266-a232bac5f7e3_1920x1025.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2><strong>Tutorial: Wiring Traces into the Lakehouse</strong></h2><h3>Sample agent: Support manager assistant</h3><p>For this blog, we&#8217;ll create a simple support manager assistant that we can use to demonstrate tracing end-to-end. The agent can be deployed outside of Databricks, as we&#8217;ve done here, highlighting that trace ingestion is decoupled from where the agent runs.</p><p>We built a LangGraph agent powered by a <a href="https://docs.databricks.com/aws/en/machine-learning/foundation-model-apis/supported-models#-anthropic-claude-sonnet-4">Databricks-hosted Claude Sonnet 4 model</a> for reasoning and response generation. The agent calls a Genie Space as a tool, which you can deploy <a href="https://www.databricks.com/resources/demos/tutorials/aibi-customer-support-review-dashboards-and-genie?itm_data=demo_center&amp;itm_source=www&amp;itm_category=resources&amp;itm_page=tutorials&amp;itm_location=Data%20Warehouse%20and%20BI&amp;itm_component=card&amp;itm_offer=aibi-customer-support-review-dashboards-and-genie">here</a>.</p><p>When a user asks a data-driven question, the agent invokes Genie through the MCP tool API. Genie translates the request into SQL, executes it against the support dataset, and returns the result. The agent then summarizes the findings and provides actionable takeaways for a support manager.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ye7I!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ye7I!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png 424w, https://substackcdn.com/image/fetch/$s_!ye7I!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png 848w, https://substackcdn.com/image/fetch/$s_!ye7I!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png 1272w, https://substackcdn.com/image/fetch/$s_!ye7I!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ye7I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png" width="667" height="111" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:111,&quot;width&quot;:667,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:14975,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/188328490?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ye7I!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png 424w, https://substackcdn.com/image/fetch/$s_!ye7I!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png 848w, https://substackcdn.com/image/fetch/$s_!ye7I!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png 1272w, https://substackcdn.com/image/fetch/$s_!ye7I!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99fd4f6b-3002-49d2-bd64-dba9754731fc_667x111.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h3>Setting up MLflow tracing with UC</h3><p>Before instrumenting the agent, we first configure MLflow to store traces in Unity Catalog. This involves creating the underlying OpenTelemetry tables and linking them to an MLflow experiment so traces can be searched, analyzed, and annotated from the UI. Start by identifying (or creating) a SQL warehouse and an MLflow experiment, then use the MLflow Python library to create the Unity Catalog tables and link the schema to the experiment. For full steps, follow the docs <a href="https://docs.databricks.com/aws/en/mlflow3/genai/tracing/trace-unity-catalog">here</a>.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;db906a00-42d5-4fd1-addd-154efbb0f3dd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">import os
import mlflow
from mlflow.entities import UCSchemaLocation
from mlflow.tracing.enablement import set_experiment_trace_location

mlflow.set_tracking_uri("databricks")

os.environ["MLFLOW_TRACING_SQL_WAREHOUSE_ID"] = "&lt;warehouse-id&gt;"

experiment_name = "&lt;experiment-name&gt;"
catalog_name = "&lt;catalog&gt;"
schema_name = "&lt;schema&gt;"

experiment_id = mlflow.create_experiment(name=experiment_name)

set_experiment_trace_location(
    location=UCSchemaLocation(
        catalog_name=catalog_name,
        schema_name=schema_name,
    ),
    experiment_id=experiment_id,
)</code></pre></div><p>This setup creates Unity Catalog tables for spans, logs, and metrics. Once traces begin flowing, the MLflow service also creates Databricks views that transform the underlying OpenTelemetry data into an MLflow-friendly format for easier querying and analysis. These include:</p><ul><li><p><strong>mlflow_experiment_trace_otel_spans</strong>: detailed execution steps for each request</p></li><li><p><strong>mlflow_experiment_trace_otel_logs</strong>: structured events such as metadata, tags, and assessments</p></li><li><p><strong>mlflow_experiment_trace_otel_metrics</strong>: numerical telemetry captured during execution</p></li><li><p><strong>mlflow_experiment_trace_metadata</strong>: MLflow tags, metadata, and assessments grouped by trace ID</p></li><li><p><strong>mlflow_experiment_trace_unified</strong>: a consolidated view that assembles all trace data into a single record per trace. For better performance at scale, consider converting it to a materialized view with incremental refresh.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!0mPM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!0mPM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png 424w, https://substackcdn.com/image/fetch/$s_!0mPM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png 848w, https://substackcdn.com/image/fetch/$s_!0mPM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png 1272w, https://substackcdn.com/image/fetch/$s_!0mPM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!0mPM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png" width="790" height="276" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3bd12fad-1472-4d95-86d6-af315e542030_790x276.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:276,&quot;width&quot;:790,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!0mPM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png 424w, https://substackcdn.com/image/fetch/$s_!0mPM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png 848w, https://substackcdn.com/image/fetch/$s_!0mPM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png 1272w, https://substackcdn.com/image/fetch/$s_!0mPM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3bd12fad-1472-4d95-86d6-af315e542030_790x276.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>After configuring the trace destination, agent instrumentation remains the same. You can do automatic and/or manual tracing as described <a href="https://docs.databricks.com/aws/en/mlflow3/genai/tracing/app-instrumentation/">here</a>. In our example, we rely on <code>mlflow.langchain.autolog()</code> to capture the detailed LangGraph execution (model calls and tool calls). We also wrap the entrypoint with <code>@mlflow.trace</code> to establish a request-level root span, allowing each invocation to be observed as a single end-to-end execution.</p><h3>Inspecting a sample trace</h3><p>Now that the agent is instrumented and traces are flowing into Unity Catalog, let&#8217;s look at a real execution.</p><p>For this example, we asked the Support Manager Assistant:</p><blockquote><p>&#8220;Which support engineer should I put up for promotion?&#8221;</p></blockquote><p>The agent evaluated the request, called the Genie space multiple times to gather supporting data, and returned a recommendation based on performance metrics.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AcEM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AcEM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png 424w, https://substackcdn.com/image/fetch/$s_!AcEM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png 848w, https://substackcdn.com/image/fetch/$s_!AcEM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png 1272w, https://substackcdn.com/image/fetch/$s_!AcEM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AcEM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png" width="1210" height="560" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:560,&quot;width&quot;:1210,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AcEM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png 424w, https://substackcdn.com/image/fetch/$s_!AcEM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png 848w, https://substackcdn.com/image/fetch/$s_!AcEM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png 1272w, https://substackcdn.com/image/fetch/$s_!AcEM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2a476c30-e51f-4298-977e-ad5a85543aa4_1210x560.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>While the response looks straightforward, the trace reveals the underlying execution path that produced it. In the MLflow experiment, we can see each of the tool calls as well as the reasoning logic of our claude sonnet model. We can see that it called the genie space tool three times before putting together a final answer.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6G7s!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6G7s!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png 424w, https://substackcdn.com/image/fetch/$s_!6G7s!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png 848w, https://substackcdn.com/image/fetch/$s_!6G7s!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png 1272w, https://substackcdn.com/image/fetch/$s_!6G7s!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6G7s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png" width="488" height="623" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:623,&quot;width&quot;:488,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!6G7s!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png 424w, https://substackcdn.com/image/fetch/$s_!6G7s!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png 848w, https://substackcdn.com/image/fetch/$s_!6G7s!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png 1272w, https://substackcdn.com/image/fetch/$s_!6G7s!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0d552252-5c34-46b2-a430-cc0f8c7b7504_488x623.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We can click through each of the individual steps to study the inputs and outputs.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ejSG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ejSG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png 424w, https://substackcdn.com/image/fetch/$s_!ejSG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png 848w, https://substackcdn.com/image/fetch/$s_!ejSG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png 1272w, https://substackcdn.com/image/fetch/$s_!ejSG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ejSG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png" width="1056" height="581" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:581,&quot;width&quot;:1056,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ejSG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png 424w, https://substackcdn.com/image/fetch/$s_!ejSG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png 848w, https://substackcdn.com/image/fetch/$s_!ejSG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png 1272w, https://substackcdn.com/image/fetch/$s_!ejSG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a107d80-9a43-49d6-8042-c0bc5fc89184_1056x581.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Because traces are stored as Delta tables, they can be queried like any other dataset. We can start with the <code>mlflow_experiment_trace_unified</code> view, where we will find a record that includes the request, response, trace metadata, and an array of the spans.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mhqL!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mhqL!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png 424w, https://substackcdn.com/image/fetch/$s_!mhqL!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png 848w, https://substackcdn.com/image/fetch/$s_!mhqL!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png 1272w, https://substackcdn.com/image/fetch/$s_!mhqL!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mhqL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png" width="779" height="438" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:438,&quot;width&quot;:779,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:68062,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/188328490?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mhqL!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png 424w, https://substackcdn.com/image/fetch/$s_!mhqL!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png 848w, https://substackcdn.com/image/fetch/$s_!mhqL!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png 1272w, https://substackcdn.com/image/fetch/$s_!mhqL!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F151eb393-06db-488f-9f9f-f8552c1dc125_779x438.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><h2>Beyond Debugging: Analytics on Trace Data</h2><p>Now that traces are stored in Unity Catalog, they become immediately available for both batch and streaming analytics.</p><h3>Governance in Unity Catalog</h3><p>Prompts and responses, however, often contain sensitive information, so treating trace data as governed data is critical. By storing it in Unity Catalog, traces inherit fine-grained access controls, from catalog and schema permissions to column masking and row-level filtering,  enabling secure, production-ready analytics without limiting flexibility.</p><p>Once access is established, teams can securely run ad-hoc analytics by querying the underlying tables and views with SQL, as we did above. We can also build ETL pipelines, in addition to dashboards and genie spaces, for actionable business insights.</p><h3>Dashboards</h3><p>One of the most powerful aspects of having traces in Unity Catalog is that we aren&#8217;t locked into a vendor&#8217;s rigid, pre-canned views. Because the traces are in Delta tables, we can build custom dashboards that reflect our specific business logic, not just generic system health.</p><p>Using AI/BI Dashboards, we built an<strong> <a href="https://github.com/brunohub/mlflow-traces-observability/tree/main">AI Operations Center</a> </strong>that sits directly on top of our trace tables. This dashboard provides a unified view of our application performance, costs, and reliability. Instead of learning a proprietary query language, we just wrote standard SQL (with the help of <a href="https://www.databricks.com/blog/introducing-databricks-assistant-data-science-agent">AI</a>) to extract exactly what we needed.</p><p>Here are some key capabilities this unlocked:</p><p><strong>Custom Cost &amp; Token Analysis</strong> <br>Generic &#8220;cost&#8221; metrics are rarely accurate because every team negotiates different rates or uses fine-tuned models with unique pricing. Since we control the SQL, we embedded our specific pricing logic directly into the query. Our dashboard tracks token usage by model type (e.g., GPT-4o vs. Claude 4 Sonnet) and applies our contract-specific rates to calculate a precise <strong>Estimated Cost per Trace</strong>. This lets us spot expensive outliers immediately&#8212;like a single complex query that costs $0.50 due to a retrieval loop.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!czXE!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!czXE!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png 424w, https://substackcdn.com/image/fetch/$s_!czXE!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png 848w, https://substackcdn.com/image/fetch/$s_!czXE!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png 1272w, https://substackcdn.com/image/fetch/$s_!czXE!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!czXE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png" width="1041" height="708" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:708,&quot;width&quot;:1041,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!czXE!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png 424w, https://substackcdn.com/image/fetch/$s_!czXE!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png 848w, https://substackcdn.com/image/fetch/$s_!czXE!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png 1272w, https://substackcdn.com/image/fetch/$s_!czXE!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F004f1220-a2ad-48cd-97a9-c8e627211d33_1041x708.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p><strong>Component-Level Performance</strong></p><p>High-level latency metrics often hide the real culprit. Is the bottleneck the LLM or is it the Genie space retrieval? We built a <strong>&#8220;Tool Performance&#8221;</strong> widget that breaks down latency (P50, P99) and error rates for every individual tool in our agent (e.g., retrieve_docs vs. generate_response). This allows us to pinpoint exactly which step in the chain is degrading the user experience.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lJfx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lJfx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png 424w, https://substackcdn.com/image/fetch/$s_!lJfx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png 848w, https://substackcdn.com/image/fetch/$s_!lJfx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png 1272w, https://substackcdn.com/image/fetch/$s_!lJfx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lJfx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png" width="1310" height="718" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:718,&quot;width&quot;:1310,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lJfx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png 424w, https://substackcdn.com/image/fetch/$s_!lJfx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png 848w, https://substackcdn.com/image/fetch/$s_!lJfx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png 1272w, https://substackcdn.com/image/fetch/$s_!lJfx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f860e1-86b0-4285-8c96-cfc77d290f24_1310x718.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>Genie spaces</h3><p>Both business and technical stakeholders often want to explore agent behavior without writing SQL. By exposing trace tables through Genie, teams can enable natural-language analysis over their telemetry data, allowing users to ask questions about performance, tool usage, latency, and model behavior directly. In our example, this could include questions such as:</p><ul><li><p>What types of requests require escalation?</p></li><li><p>Are tool retries increasing?</p></li><li><p>Which queries trigger the most complex execution paths?</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xlNf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xlNf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png 424w, https://substackcdn.com/image/fetch/$s_!xlNf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png 848w, https://substackcdn.com/image/fetch/$s_!xlNf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png 1272w, https://substackcdn.com/image/fetch/$s_!xlNf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xlNf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png" width="920" height="437" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:437,&quot;width&quot;:920,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xlNf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png 424w, https://substackcdn.com/image/fetch/$s_!xlNf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png 848w, https://substackcdn.com/image/fetch/$s_!xlNf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png 1272w, https://substackcdn.com/image/fetch/$s_!xlNf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5d855164-9878-4f7a-90c5-3540ab887ef9_920x437.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3>ETL pipelines</h3><p>Because traces are stored as Delta tables, they can feed downstream ETL pipelines just like any other dataset. By enabling <a href="https://docs.databricks.com/aws/en/delta/delta-change-data-feed">Change Data Feed (CDF)</a>, teams can process trace data incrementally, either in batch or streaming, without repeatedly scanning entire tables.</p><p>This makes it possible to operationalize observability. For example, a pipeline could monitor trace patterns and trigger alerts when latency exceeds defined thresholds, tool failures spike, or token usage deviates from expected baselines. These signals can then feed dashboards, notification systems, or automated remediation workflows.</p><p>Importantly, this complements real-time protections such as <a href="https://docs.databricks.com/aws/en/ai-gateway/overview-serving-endpoints#ai-guardrails">AI Guardrails</a>. While guardrails enforce policy at request time, ETL pipelines create a feedback loop, helping teams analyze trends, refine policies, and continuously improve agent performance.</p><p></p><h2>Closing the Loop: From Production Traces to Evaluation</h2><p>Once traces are available, they can power the full MLflow 3 <a href="https://docs.databricks.com/aws/en/mlflow3/genai/eval-monitor/">evaluation stack</a>, enabling teams to measure, improve, and maintain the quality of their AI applications across the entire lifecycle. Evaluation and monitoring build directly on tracing, allowing the same telemetry captured during development, testing, and production to be scored using LLM judges and custom metrics.</p><h3>Evaluate during development using AI Judges</h3><p>MLflow allows us to run evaluations against an evaluation dataset, applying built-in or custom judges to score response quality. One effective approach is to bootstrap this dataset from real traces. Because these prompts originate from actual user interactions, they better represent the scenarios your agent must handle compared to synthetic test cases.</p><p>Below, we create an evaluation dataset from recently captured traces. MLflow uses a SQL warehouse to search and materialize dataset records, so be sure to configure the warehouse ID in your environment.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;bc030680-8dd0-46fd-8220-29d726db3488&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import os
import mlflow
import mlflow.genai.datasets
import time

# Required for dataset operations
os.environ["MLFLOW_TRACING_SQL_WAREHOUSE_ID"] = MLFLOW_TRACING_SQL_WAREHOUSE_ID

DATASET_NAME = f"{CATALOG_NAME}.{SCHEMA_NAME}.support_management_chatbot_traces"

# Create (or load) the dataset
try:
    eval_dataset = mlflow.genai.datasets.create_dataset(name=DATASET_NAME)
except Exception:
    eval_dataset = mlflow.genai.get_dataset(name=DATASET_NAME)

# Pull recent traces (example - from yesterday)
yesterday = int((time.time() - 60 * 60 * 24) * 1000)

traces_df = mlflow.search_traces(
    filter_string=f"attributes.timestamp_ms &gt; {yesterday}",
    order_by=["attributes.timestamp_ms DESC"],
)

# Merge traces into the dataset
eval_dataset = eval_dataset.merge_records(traces_df[["inputs"]])</code></pre></div><p>With the dataset in place, we can define the judges that will score our application. MLflow provides a set of built-in judges, and also allows us to define custom guidelines tailored to our agent&#8217;s expected behavior.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;542a1e40-6a26-40e3-a001-a638c4f625fc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">from mlflow.genai.scorers import RelevanceToQuery, Safety, Guidelines

# Define judges
agent_judges = [
    RelevanceToQuery(),
    Guidelines(
        name="analytical_correctness",
        guidelines="The response must correctly interpret the data and avoid unsupported conclusions.",
    ),
    Guidelines(
        name="actionable_support_insights",
        guidelines="The response must provide at least one concrete, data-backed recommendation.",
    ),
    Guidelines(
        name="performance_management",
        guidelines="The response should not recommend admonishing or firing employees.",
    ),
    Safety(),
]

# Run evaluation
eval_results = mlflow.genai.evaluate(
    data=eval_dataset,
    predict_fn=predict_fn,
    scorers=agent_judges,
)

eval_results</code></pre></div><p>And we can now see the results in the MLflow experiment.</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!JMne!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!JMne!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png 424w, https://substackcdn.com/image/fetch/$s_!JMne!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png 848w, https://substackcdn.com/image/fetch/$s_!JMne!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png 1272w, https://substackcdn.com/image/fetch/$s_!JMne!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!JMne!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png" width="1332" height="319" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:319,&quot;width&quot;:1332,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:69873,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/188328490?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!JMne!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png 424w, https://substackcdn.com/image/fetch/$s_!JMne!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png 848w, https://substackcdn.com/image/fetch/$s_!JMne!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png 1272w, https://substackcdn.com/image/fetch/$s_!JMne!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffd855155-6d0d-4355-91aa-bddada2a1bdc_1332x319.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><h3>Production monitoring</h3><p>Development evaluations help us validate behavior before release, but production monitoring shows us how the application performs with real users. MLflow can automatically evaluate live traces using the same judges, helping us quickly detect regressions, drift, and emerging failure patterns. This turns evaluation from a one-time task into an ongoing practice as the application evolves.</p><p></p><h2>Frequently Asked Questions (FAQ)</h2><ul><li><p><strong>Can I use this for agents running outside of Databricks?</strong></p><p>Yes, the agent can be running anywhere. In fact the support assistant agent example that was used for this blog is deployed locally.</p></li><li><p><strong>What are the throughput and storage limits of this solution?</strong></p><p>The ingestion throughput <a href="https://docs.databricks.com/aws/en/mlflow3/genai/tracing/trace-unity-catalog#-limitations">limit is 200 QPS</a> today. There is no limit on storage. Previous limits on traces per experiment are no longer applicable. If you need higher throughput limits, please reach out to your Databricks account team.</p></li><li><p><strong>What can I do to ensure my search queries, MLflow experiment experience, and downstream analytics remain performant?</strong></p><p>Consider optimizing the OTEL tables using Z-ordering as described <a href="https://docs.databricks.com/aws/en/mlflow3/genai/tracing/observe-with-traces/query-dbsql#performance-considerations">here</a>.</p></li><li><p><strong>How does this handle PII found in user prompts?</strong></p><p>This feature does not apply any special handling to PII. However, the data is stored in Unity Catalog, where you can leverage governance capabilities, such as fine-grained access controls, column masking, and row filtering, to manage and restrict downstream access.</p></li></ul><p></p><h2>Get started</h2><p>To get started, follow along with the <a href="https://docs.databricks.com/aws/en/mlflow3/genai/tracing/trace-unity-catalog">documentation</a>.</p>]]></content:encoded></item><item><title><![CDATA[Trace your steps back to Slack]]></title><description><![CDATA[Create a slackbot to to review MLflow traces for your agent.]]></description><link>https://www.databricksters.com/p/trace-your-steps-back-to-slack</link><guid isPermaLink="false">https://www.databricksters.com/p/trace-your-steps-back-to-slack</guid><dc:creator><![CDATA[Veena]]></dc:creator><pubDate>Tue, 25 Nov 2025 16:02:25 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Li1P!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you have been creating and deploying Agents on Databricks, then perhaps you are already aware of the existence of MLflow Review Apps. For those who have not used them before, MLflow Review Apps are an easy way to collect feedback from your Subject Matter Experts on your agent. Databricks provides support for using review apps through the built-in interface or, if you need more customization, through a <a href="https://github.com/databricks-solutions/custom-mlflow-review-app/tree/main">custom review app</a> hosted on Databricks Apps.</p><p>But what if we could just bring this process directly to Slack? This blog post will walk you through building a Slackbot that enables real-time agent interaction and feedback collection.</p><h2>How does tracing and feedback work in MLflow?</h2><p>With MLflow Production Monitoring, you can see traces arrive directly in an MLflow experiment. These traces can be synced to a table in Unity Catalog.</p><p>Each trace has a unique ID automatically generated by MLflow. This ID can be used to add feedback (source: <a href="https://docs.databricks.com/aws/en/mlflow3/genai/tracing/collect-user-feedback/#implementing-feedback-collection">Databricks documentation</a>) via the MLflow <code>log_feedback</code> function. This can be an LLM judge or human feedback. Feedback is also stored as an assessment linked to the specific trace, making it queryable through MLflow.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Li1P!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Li1P!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png 424w, https://substackcdn.com/image/fetch/$s_!Li1P!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png 848w, https://substackcdn.com/image/fetch/$s_!Li1P!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png 1272w, https://substackcdn.com/image/fetch/$s_!Li1P!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Li1P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png" width="803" height="1125" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/aff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1125,&quot;width&quot;:803,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Li1P!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png 424w, https://substackcdn.com/image/fetch/$s_!Li1P!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png 848w, https://substackcdn.com/image/fetch/$s_!Li1P!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png 1272w, https://substackcdn.com/image/fetch/$s_!Li1P!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faff6dc68-c00c-4aca-8334-fd93b8d4170e_803x1125.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A labeling session (source: <a href="https://docs.databricks.com/aws/en/mlflow3/genai/human-feedback/concepts/labeling-sessions">Databricks documentation</a>) is a special type of run within MLflow. Databricks recommends adding specific traces to a labelling session beforehand-- the custom or built-in review app then connects to that labeling session and exposes the traces to SMEs. The app allows us to just interact with the MLflow client in a specific way. This requires us to pre-select traces.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!S6YX!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!S6YX!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png 424w, https://substackcdn.com/image/fetch/$s_!S6YX!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png 848w, https://substackcdn.com/image/fetch/$s_!S6YX!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png 1272w, https://substackcdn.com/image/fetch/$s_!S6YX!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!S6YX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png" width="1456" height="764" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:764,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!S6YX!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png 424w, https://substackcdn.com/image/fetch/$s_!S6YX!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png 848w, https://substackcdn.com/image/fetch/$s_!S6YX!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png 1272w, https://substackcdn.com/image/fetch/$s_!S6YX!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9af50aad-ee37-4f49-89f4-5d0e94030a9f_1600x840.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>To create a Slackbot that can perform the same tasks as a custom review app, we will need to host it on a Databricks App. In this app, we are going to use labeling sessions slightly differently. Instead of interacting with pre-selected traces, we will allow SMEs to interact with the agent directly, creating traces and adding them to an already- created labeling session immediately. Then, the SME can add feedback via Slack interactions.</p><h1>Building the Slackbot Review App</h1><p><a href="https://github.com/veenaramesh/custom-slack-review-app">Follow along with the code here. </a></p><p>This is the experience we want:</p><ol><li><p>Human experts ask questions in a Slack channel.</p></li><li><p>The agent answers the question in the same Slack thread.</p></li><li><p>Human experts provide feedback via Slack shortcuts.</p></li></ol><p>Therefore, our Slackbot should:</p><ol><li><p>Listen to messages in Slack.</p></li><li><p>Call our agent in Databricks.</p></li><li><p>Collect feedback from SMEs in Slack.</p></li><li><p>Annotate MLflow traces with that feedback.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!7MQj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!7MQj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png 424w, https://substackcdn.com/image/fetch/$s_!7MQj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png 848w, https://substackcdn.com/image/fetch/$s_!7MQj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png 1272w, https://substackcdn.com/image/fetch/$s_!7MQj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!7MQj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png" width="1174" height="626" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:626,&quot;width&quot;:1174,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!7MQj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png 424w, https://substackcdn.com/image/fetch/$s_!7MQj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png 848w, https://substackcdn.com/image/fetch/$s_!7MQj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png 1272w, https://substackcdn.com/image/fetch/$s_!7MQj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcd9a906f-1769-47fe-a536-bd05ea1145d1_1174x626.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div></li></ol><h2>Some housekeeping</h2><p>Before we get started with writing the Databricks app, we will first need to create the following: </p><h3>Creating an app in Slack</h3><p>First, let&#8217;s create an application in Slack. For more detailed instructions, <a href="https://medium.com/m/global-identity-2?redirectUrl=https%3A%2F%2Fpython.plainenglish.io%2Flets-create-a-slackbot-cause-why-not-2972474bf5c1">check out this Medium blog post.</a></p><p>I have included the app manifest for the Slackbot with all necessary configurations, but check out the necessary scope and permissions for the bot. We will definitely need the scopes: (1) chat:write (2) groups:read (3) im:read (4) mpim:history (5) commands.</p><p>Once you have installed the app in Slack, you will be given a Bot User oauth token. Save this securely. We will need to use that in our app.</p><h3>Creating a Databricks App</h3><p>Databricks Apps makes hosting straightforward, as each app has an associated Service Principal. All we need to do is ensure that the Service Principal has access to our MLflow experiment and agent endpoint.</p><p>Using the CLI, we can create the app:</p><p><code>databricks apps create slackbot</code></p><p>Sync local files to the Databricks workspace:</p><p><code>databricks sync . &#8220;/Users/$DATABRICKS_USERNAME/slackbot-app&#8221;</code></p><p>Then, deploy:</p><p><code>databricks apps deploy slackbot --source-code-path /Workspace/Users/$DATABRICKS_USERNAME/agent-proto</code></p><h3>Creating a MLflow labeling session</h3><p>We should also create a labeling session that we will use within our MLflow experiment. This creates a persistent Mlflow run that we will link all Slack-generated traces to. You can do this in a notebook with the SDK or through the MLflow experiment UI. </p><pre><code>import mlflow.genai.labeling as labeling

import mlflow.genai.label_schemas as schemas

# Create a simple labeling session with built-in schemas

session = labeling.create_labeling_session(
    name=&#8221;customer_service_review_jan_2024&#8221;, 
    assigned_users=[&#8221;alice@company.com&#8221;, &#8220;bob@company.com&#8221;],
    label_schemas=[schemas.EXPECTED_FACTS]  
    # Required: at least one schema needed 
)</code></pre><p>Source: <a href="https://docs.databricks.com/aws/en/mlflow3/genai/human-feedback/concepts/labeling-sessions">Databricks documentation.</a></p><h2>1. Initializing the Slack client</h2><p>In our Databricks App, using the Slack SDK, we can easily connect to our Slack App:</p><pre><code>def get_slack_auth():
    w = WorkspaceClient()
    token_bot = dbutils.secrets.get(scope=&#8221;brickbrain-scope&#8221;, key=&#8221;slack-bot-token&#8221;)    
    return token_bot

def start_slack_client():
    logger.info(&#8221;Initalized slack client. &#8220;)
    ssl_context = ssl.create_default_context()
    ssl_context.check_hostname = False
    ssl_context.verify_mode = ssl.CERT_NONE
    token_bot = get_slack_auth()
    client = slack_sdk.WebClient(token=token_bot, ssl=ssl_context)
    return App(client=client, process_before_response=False)

app = start_slack_client()
</code></pre><p>Note: store the Slack token in Databricks Secrets for security. Ensure your Service Principal has permissions to that secret scope.</p><h2>2. Listening to events</h2><p>Depending on the permissions given to your application, your slackbot will be able to receive and be able to respond to different events. Take a look at the full list of the events (source: <a href="https://docs.slack.dev/reference/events/">Slack documentation</a>). </p><p>First, let&#8217;s take a look at the message event, which observes whether or not a message was sent to a channel. In the example, I am observing every event that is sent to a channel. If you want to minimize the scope, you can select a message subtype or naively use string manipulation. I am going to be using <a href="https://docs.slack.dev/tools/bolt-python/">slack-bolt</a> moving forward to respond and take actions as the bot.</p><p>Bolt has many decorators that we can use to listen or observe events. For example, when observing the message event, I can declare the following:</p><pre><code>@app.event(&#8221;message&#8221;)
def llm_response(event, say, client):
    logger.info(f&#8221;Message received - User: {event[&#8217;user&#8217;]}, Text: {event[&#8217;text&#8217;][:20]}...&#8221;)
   &lt;...&gt;</code></pre><p>For different types of &#8220;listeners&#8221;, we can have different function arguments: </p><ul><li><p><code>payload</code>: also accessible via the alias corresponding to the method name that the listener is passed to (message, event, action, shortcut, view, command, options).</p><ul><li><p>In this case, <code>event</code> == payload</p></li></ul></li><li><p><code>say</code>: function send a message to the channel associated with the event.</p></li><li><p><code>ack</code>: function that must be acknowledged that an incoming event was received by the app.</p></li><li><p><code>client</code>: web API client that uses the token associated with that event.</p></li><li><p><code>logger</code></p></li></ul><p>This is not a complete list! But these are the most important ones for our use case (source: <a href="https://docs.slack.dev/tools/bolt-js/reference/#listener-function-arguments">Slack documentation</a>). </p><h2>3. Calling the agent</h2><p>In our app, we want to respond to messages sent to the channel. We can easily trigger an LLM call now. However, in order to add feedback to the trace, we need to get the trace ID. When interacting with a Databricks endpoint, we can do this by setting the variable <code>return_trace</code> to True. </p><pre><code>        input_data = {
            &#8220;input&#8221;: history + [{&#8221;role&#8221;: &#8220;user&#8221;, &#8220;content&#8221;:  message_text}],
            &#8220;databricks_options&#8221;: {&#8221;return_trace&#8221;: True}
        }

        response = mlflow_client.predict(endpoint=ENDPOINT_NAME, inputs=input_data)</code></pre><p>The response output will then give me the trace ID: </p><pre><code>        trace_id = response[&#8217;databricks_output&#8217;][&#8217;trace&#8217;][&#8217;info&#8217;][&#8217;trace_id&#8217;]</code></pre><h2>4. Responding to the message</h2><p>To respond within a thread, we will need to use the client API. Recall that the listener argument &#8220;say&#8221; is offered with most events. However, &#8220;say&#8221; does not allow us to respond within a thread. </p><p>LLMs often use and produce Markdown as an output format. It is important to note that Slack uses its own markdown language, and although most basic syntax support is provided, some elements are absent. Take a look at what is supported <a href="https://www.markdownguide.org/tools/slack/.">here</a>.</p><p>If you want to ensure that the output is stylized in the same way that the LLM intended, I would suggest looking at manually converting the Markdown text into Slack&#8217;s mrkdwn format. This would require some string manipulation with regex (source: <a href="https://github.com/fla9ua/markdown_to_mrkdwn">Github repo</a>).</p><pre><code>    result = client.chat_postMessage(
        channel=event[&#8217;channel&#8217;],
        blocks=[
            {
                &#8220;type&#8221;: &#8220;section&#8221;,
                &#8220;text&#8221;: {&#8221;type&#8221;: &#8220;mrkdwn&#8221;, &#8220;text&#8221;: slack_response}
            },
        ],
        text=slack_response,
        thread_ts=event[&#8217;ts&#8217;],  # reply in the thread
        metadata={
            &#8220;event_type&#8221;: &#8220;agent_response&#8221;,
            &#8220;event_payload&#8221;: {
                &#8220;trace_id&#8221;: trace_id, # trace id in metadata
                &#8220;thread_id&#8221;: event[&#8217;ts&#8217;],
                &#8220;resource_type&#8221;: &#8220;AGENT_RESPONSE&#8221;,
            }
        }
    )</code></pre><p>Using the Client API, we can also attach metadata to each message. This makes it easier to retrieve information across sessions, like <code>trace_id</code>.</p><p>We have designed the response simply, but Slack has a lot of options on how to design a Slack message. Take a look at <a href="https://app.slack.com/block-kit-builder/T02TL6JB2">Block Kit Builder</a> to see how you can structure your Slack message with buttons, dividers, images, inputs, etc. </p><h2>5. Adding feedback</h2><p>We will use a Slack message shortcut to log feedback. I found this method to be the most straightforward and easiest to customize. However, we can also use Slack message blocks to design a feedback form as well.</p><p>When I use the add_feedback shortcut, this triggers the event &#8220;message_shortcut&#8221;. Because we have added the trace id to the metadata of the agent response Slack message, we can access that trace_id in the Slack shortcut.</p><pre><code>@app.message_shortcut(&#8221;log_feedback&#8221;)
def handle_log_feedback_shortcut(ack, shortcut, client):
    ack()
    logger.info(f&#8221;Feedback message shortcut triggered by user: {shortcut[&#8217;user&#8217;][&#8217;name&#8217;]}&#8221;)
    
    message = shortcut[&#8217;message&#8217;]
    message_ts = message[&#8217;ts&#8217;]
    
    metadata = message.get(&#8217;metadata&#8217;, {})</code></pre><p>When handling this event, we can use the Client API to open a view with the formatted feedback form. We can add comments and binary feedback. These inputs will be translated as input for <code>mlflow.log_feedback()</code>. However, <code>log_feedback</code> can take all sorts of values: integers, floats, categorical values, and multiple-category feedback (source: <a href="https://docs.databricks.com/aws/en/mlflow3/genai/tracing/concepts/log-assessment">Databricks documentation</a>). So, feel free to customize this to what your evaluation system needs.</p><p>Since this is a form, once we hit submit, we will need to respond to another Slack event as well. This will create another Slack event called &#8220;view&#8221;. This is where we actually handle the feedback submission and use <code>mlflow.log_feedback().</code> For your review app, you can also log expectations (aka ground truth) using another function <code>log_expectations()</code>.</p><h2>6. Linking everything to a labeling session</h2><p>We still have not linked these traces to a labeling session. To do so, we fetch the run ID associated with the labeling session and the trace_id:</p><pre><code>def link_traces_to_run(run_id: str, trace_ids: List[str]) -&gt; Dict[str, Any]:
    creds = get_databricks_host_creds()
    url = _get_mlflow_api_url(&#8217;/traces/link-to-run&#8217;, creds=creds)
    data = {&#8217;run_id&#8217;: run_id, &#8216;trace_ids&#8217;: trace_ids}

############################
in the @app.event function: 
############################

link_traces_to_run(run_id=LABELLING_SESSION.mlflow_run_id, trace_ids=[trace_id])
logger.info(f&#8221;Traces linked to run - Run: {LABELLING_SESSION.mlflow_run_id}, Trace: {trace_id}&#8221;)
</code></pre><h2>7. Handling with conversation history</h2><p>Slack threads make conversation history management simple. Instead of requiring a database to checkpoint, we can simply fetch the threads themselves. Using the client API and the thread ID:</p><pre><code>def get_thread_messages(client, channel, thread_ts):
    response = client.conversations_replies(
        channel=channel,
        ts=thread_ts,
        inclusive=True,  # Include the parent message
        limit=10  # Max messages to retrieve
    )
    logger.info(f&#8221;Retrieved {len(response[&#8217;messages&#8217;])} messages from thread {thread_ts}&#8221;)
    return response[&#8217;messages&#8217;]</code></pre><h1>Happy reviewing!</h1><p><a href="https://github.com/veenaramesh/custom-slack-review-app">Take a look at the full implementation and code here. </a></p><p>There are no limitations in how you can use MLflow review apps! You can easily bring the feedback mechanism in MLflow to Slack, reducing any friction in the feedback process. Thanks for reading.</p>]]></content:encoded></item><item><title><![CDATA[Getting Medieval on Token Costs]]></title><description><![CDATA[A Lakebase Powered Solution to Token-Based Rate Limiting]]></description><link>https://www.databricksters.com/p/getting-medieval-on-token-costs</link><guid isPermaLink="false">https://www.databricksters.com/p/getting-medieval-on-token-costs</guid><dc:creator><![CDATA[Austin]]></dc:creator><pubDate>Fri, 24 Oct 2025 20:20:45 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!vSad!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Don&#8217;t you hate it when your employees run up a thousand dollar tab on Claude API calls inside of a week and then hit you with this look when you tell them that was the budget for the quarter?</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vSad!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vSad!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vSad!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vSad!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vSad!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vSad!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg" width="770" height="554" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:554,&quot;width&quot;:770,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:164374,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/177042002?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vSad!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg 424w, https://substackcdn.com/image/fetch/$s_!vSad!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg 848w, https://substackcdn.com/image/fetch/$s_!vSad!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!vSad!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa1173c2d-8ceb-4f48-9cf1-05423a01ef60_770x554.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I might have something for that. </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.databricksters.com/subscribe?"><span>Subscribe now</span></a></p><p>One of the cornerstones of the Databricks value-add in AI is that we are a model provider neutral platform. We offer native pay-per-token hosting for open source model families like Llama, Gemma, and GPT OSS and we have first party connections with Claude, OpenAI, and Gemini. However, if you want to control costs, our current AI Gateway offering only allows you to do so via QPM rate limiting. QPM certainly has its use cases, but the majority of companies don&#8217;t care how many times per minute their employees or end users hit a model; they care about how much it&#8217;s going to cost them.</p><p>Luckily with Lakebase, token-based rate limiting is now possible and the implementation is simple: a user submits a request, which is then validated by the endpoint via queries to two Lakebase tables, the first to determine that user&#8217;s token limits and the second to determine how far into those limits they already are. If the user is out of tokens, a cutoff message is returned and the request does not hit the FM. Otherwise, the request is passed to the FM and the payload is written back to Lakebase so that the user&#8217;s total token count is updated. Finally, the response is returned to the end user with a message noting their remaining token balance.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!KIO-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!KIO-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png 424w, https://substackcdn.com/image/fetch/$s_!KIO-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png 848w, https://substackcdn.com/image/fetch/$s_!KIO-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png 1272w, https://substackcdn.com/image/fetch/$s_!KIO-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!KIO-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png" width="1456" height="827" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/20253005-5ce7-425b-9167-94487120028d_1800x1022.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:827,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:104493,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/177042002?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!KIO-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png 424w, https://substackcdn.com/image/fetch/$s_!KIO-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png 848w, https://substackcdn.com/image/fetch/$s_!KIO-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png 1272w, https://substackcdn.com/image/fetch/$s_!KIO-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F20253005-5ce7-425b-9167-94487120028d_1800x1022.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Great, let&#8217;s see some code then, huh?</p><p>First we need to install <code>psycopg2</code>:</p><pre><code>%pip install psycopg2
dbutils.library.restartPython()</code></pre><p>And set a few environment variables from a Lakebase instance:</p><pre><code>import mlflow.pyfunc
import os

os.environ[&#8217;OPENAI_API_KEY&#8217;] = &#8216;&#8217; # or whatever FM API key
os.environ[&#8217;DATABRICKS_TOKEN&#8217;] = &#8216;&#8217;
os.environ[&#8217;POSTGRES_HOST&#8217;] = &#8216;&#8217;
os.environ[&#8217;POSTGRES_DBNAME&#8217;] = &#8216;databricks_postgres&#8217; # or &#8216;&#8217;
os.environ[&#8217;POSTGRES_USER&#8217;] = &#8216;&#8217;
os.environ[&#8217;POSTGRES_SSLMODE&#8217;] = &#8216;&#8217;
os.environ[&#8217;POSTGRES_PORT&#8217;] = 5432 # or &#8216;&#8217;
os.environ[&#8217;POSTGRES_PASSWORD&#8217;] = &#8216;&#8217;</code></pre><p>For the demonstration, let&#8217;s create a couple quick example tables and populate the <code>user_token_limits</code> table with a record:</p><pre><code>%sql
-- Create token_usage table for tracking all API calls
CREATE TABLE IF NOT EXISTS token_usage (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_name VARCHAR(255) NOT NULL,
    model_name VARCHAR(100) NOT NULL,
    prompt_tokens INTEGER NOT NULL,
    completion_tokens INTEGER NOT NULL,
    total_tokens INTEGER NOT NULL,
    request_timestamp TIMESTAMP NOT NULL,
    request_id VARCHAR(255),
    response_content STRING,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Create user_token_limits table for managing quotas
CREATE TABLE IF NOT EXISTS user_token_limits (
    id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_name VARCHAR(255) NOT NULL,
    model_name VARCHAR(100) NOT NULL,
    token_limit INTEGER NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Insert sample user limit
INSERT INTO user_token_limits (user_name, model_name, token_limit) 
VALUES (&#8217;test.user@databricks.com&#8217;, &#8216;gpt-4.1-2025-04-14&#8217;, 1000);</code></pre><p>Obviously you could do the above in the PostgreSQL editor, but might as well use the notebook since we&#8217;re here.</p><p>And now we can define our rate limiter. Note that this is <em>extremely</em> flexible. Any kind of rate limiting you can think up is doable as long as you can translate it into PostgreSQL. That means per user, per user per model, per user per model per unit time, and so on are all at your fingertips. I&#8217;m going to define a simple per user per model rate limit as hinted above and populate that with a token cutoff of just 1000 tokens on GPT 4.1:</p><pre><code>import mlflow
from mlflow.types import DataType, Schema, ColSpec
import mlflow.models
import json
import pandas as pd
import psycopg2
import requests
from datetime import datetime
import os

class TokenLimitedGatewayModel(mlflow.pyfunc.PythonModel):
    def load_context(self, context):
        &#8220;&#8221;&#8220;Initialize database connection and endpoint URL&#8221;&#8220;&#8221;
        self.conn = psycopg2.connect(
            host=os.environ[&#8217;POSTGRES_HOST&#8217;],
            dbname=os.environ[&#8217;POSTGRES_DBNAME&#8217;],
            user=os.environ[&#8217;POSTGRES_USER&#8217;],
            password=os.environ[&#8217;POSTGRES_PASSWORD&#8217;],
            port=int(os.environ.get(&#8217;POSTGRES_PORT&#8217;, 5432)),
            sslmode=os.environ.get(&#8217;POSTGRES_SSLMODE&#8217;, &#8216;require&#8217;)
        )
        self.conn.autocommit = True
        self.cursor = self.conn.cursor()
        
        # FM endpoint
        self.fm_endpoint = &#8220;&#8221;
        
        # Get API token from environment if needed
        self.api_token = os.environ.get(&#8217;DATABRICKS_TOKEN&#8217;, &#8216;&#8217;)
        print(&#8221;Model context loaded successfully&#8221;)

    def predict(self, context, model_input):
        &#8220;&#8221;&#8220;Process request with token limit checking&#8221;&#8220;&#8221;
        
        # Handle different input types
        if isinstance(model_input, pd.DataFrame):
            # Convert DataFrame to dict and get first row
            if len(model_input) &gt; 0:
                data = model_input.iloc[0].to_dict()
            else:
                return {&#8221;error&#8221;: &#8220;Empty input DataFrame&#8221;}
        elif isinstance(model_input, dict):
            data = model_input
        else:
            # Try to convert to dict
            try:
                data = dict(model_input)
            except:
                return {&#8221;error&#8221;: f&#8221;Unsupported input type: {type(model_input)}&#8221;}
        
        # Extract and parse messages
        messages = data.get(&#8221;messages&#8221;, [])
        if isinstance(messages, str):
            try:
                messages = json.loads(messages)
            except json.JSONDecodeError:
                return {&#8221;error&#8221;: &#8220;Invalid JSON in messages field&#8221;}
        
        # Extract parameters with defaults
        user_name = str(data.get(&#8221;user_name&#8221;, &#8220;test.user@databricks.com&#8221;))
        model_name = str(data.get(&#8221;model&#8221;, &#8220;gpt-4.1-2025-04-14&#8221;))
        
        # Handle max_tokens in case missing, this is on request side, not the rate limiter
        max_tokens_raw = data.get(&#8221;max_tokens&#8221;, 128)
        if pd.isna(max_tokens_raw) or max_tokens_raw is None:
            max_tokens = 128
        else:
            max_tokens = int(max_tokens_raw)
        
        # Handle temperature in case missing
        temperature_raw = data.get(&#8221;temperature&#8221;, 0.7)
        if pd.isna(temperature_raw) or temperature_raw is None:
            temperature = 0.7
        else:
            temperature = float(temperature_raw)
        
        # Check current token usage
        self.cursor.execute(&#8221;&#8220;&#8221;
            SELECT COALESCE(SUM(total_tokens), 0) as total_used
            FROM token_usage 
            WHERE user_name = %s AND model_name = %s
        &#8220;&#8221;&#8220;, (user_name, model_name))
        
        result = self.cursor.fetchone()
        tokens_used = int(result[0]) if result and result[0] else 0
        
        # Check user&#8217;s token limit
        self.cursor.execute(&#8221;&#8220;&#8221;
            SELECT token_limit 
            FROM user_token_limits 
            WHERE user_name = %s AND model_name = %s
        &#8220;&#8221;&#8220;, (user_name, model_name))
        
        limit_result = self.cursor.fetchone()
        
        if not limit_result:
            return {&#8221;error&#8221;: f&#8221;No token limit found for user {user_name} and model {model_name}&#8221;}
        
        token_limit = int(limit_result[0])
        
        # Check if limit exceeded
        if tokens_used &gt;= token_limit:
            return {
                &#8220;error&#8221;: f&#8221;Token limit exceeded. Used: {tokens_used}, Limit: {token_limit}&#8221;,
                &#8220;tokens_used&#8221;: tokens_used,
                &#8220;token_limit&#8221;: token_limit
            }
        
        # Prepare request for FM endpoint
        fm_request = {
            &#8220;messages&#8221;: messages,
            &#8220;max_tokens&#8221;: max_tokens,
            &#8220;temperature&#8221;: temperature
        }
        
        headers = {
            &#8220;Content-Type&#8221;: &#8220;application/json&#8221;
        }
        
        if self.api_token:
            headers[&#8221;Authorization&#8221;] = f&#8221;Bearer {self.api_token}&#8221;
        
        try:
            # Call FM endpoint
            response = requests.post(
                self.fm_endpoint,
                json=fm_request,
                headers=headers,
                timeout=30
            )
            response.raise_for_status()
            
            fm_response = response.json()
            
            # Extract token usage from response
            usage = fm_response.get(&#8221;usage&#8221;, {})
            prompt_tokens = int(usage.get(&#8221;prompt_tokens&#8221;, 0))
            completion_tokens = int(usage.get(&#8221;completion_tokens&#8221;, 0))
            total_tokens = int(usage.get(&#8221;total_tokens&#8221;, 0))
            
            # Log token usage to database
            self.cursor.execute(&#8221;&#8220;&#8221;
                INSERT INTO token_usage (
                    user_name, 
                    model_name, 
                    prompt_tokens, 
                    completion_tokens, 
                    total_tokens, 
                    request_timestamp,
                    request_id,
                    response_content
                ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
            &#8220;&#8221;&#8220;, (
                user_name,
                model_name,
                prompt_tokens,
                completion_tokens,
                total_tokens,
                datetime.utcnow(),
                fm_response.get(&#8221;id&#8221;, &#8220;&#8221;),
                json.dumps(fm_response)
            ))
            
            # Add usage info to response
            fm_response[&#8221;usage_info&#8221;] = {
                &#8220;tokens_used_total&#8221;: tokens_used + total_tokens,
                &#8220;token_limit&#8221;: token_limit,
                &#8220;tokens_remaining&#8221;: token_limit - (tokens_used + total_tokens)
            }
            
            return fm_response
            
        except requests.exceptions.RequestException as e:
            return {
                &#8220;error&#8221;: f&#8221;Failed to call FM endpoint: {str(e)}&#8221;,
                &#8220;tokens_used&#8221;: tokens_used,
                &#8220;token_limit&#8221;: token_limit
            }
        except Exception as e:
            return {
                &#8220;error&#8221;: f&#8221;Unexpected error: {str(e)}&#8221;,
                &#8220;tokens_used&#8221;: tokens_used,
                &#8220;token_limit&#8221;: token_limit
            }</code></pre><p>The astute among you will draw attention to any of the following annoyances:</p><ol><li><p>Now I need to pay for a custom model serving endpoint on top of my token costs to the foundation model(s), that&#8217;s so counterproductive!</p><ol><li><p>Ok fair, but a minimum provisioned CPU endpoint costs $0.28 per hour and can handle a relatively large request volume since it&#8217;s not actually performing any calculations except a simple comparison operation to check your token limits. If you have hundreds of users calling this endpoint per second, then yeah it might break, but for a lot of companies this guaranteed hit of $0.28/hr is worth the protection against a potentially much larger bill if some of my employees run up a huge tab without me knowing.</p></li></ol></li><li><p>This is going to add latency, and at scale I can&#8217;t abide this</p><ol><li><p>Also fair, and I would say bulk queries should certainly be run through <code>ai_query()</code> to obtain serious scale via parallel requests, but what about all your tinkerers? Your BI Analysts, your data scientists, your citizen GenAI practitioners, etc.? Are they hitting 100 QPS?</p></li></ol></li><li><p>Every new model is going to require me to set up a new config, ain&#8217;t nobody got time for that</p><ol><li><p>Yes, but, per user per model rate limiting is just an example I used to show how much specificity you could add to this if and only if you wanted to. You could instead set this up one time to handle requests to any of the main endpoints your employees are calling (new model additions are likely to follow the same API patterns as their predecessors) and only limit per user or per user per unit time. This simplifies the deployment and management.</p></li></ol></li></ol><p>With the totally reasonable objections out of the way, let&#8217;s log and register this thing and then I&#8217;ll leave you with a couple concluding thoughts:</p><pre><code># Define signature - all fields required
input_schema = Schema([
    ColSpec(DataType.string, &#8220;messages&#8221;),
    ColSpec(DataType.string, &#8220;user_name&#8221;),
    ColSpec(DataType.string, &#8220;model&#8221;),
    ColSpec(DataType.long, &#8220;max_tokens&#8221;),
    ColSpec(DataType.double, &#8220;temperature&#8221;)
])

output_schema = Schema([
    ColSpec(DataType.string, &#8220;response&#8221;)
])

signature = mlflow.models.ModelSignature(
    inputs=input_schema,
    outputs=output_schema
)

pip_requirements = [
    &#8220;mlflow&#8221;,
    &#8220;requests&#8221;,
    &#8220;psycopg2-binary&#8221;,
    &#8220;pandas&#8221;
]


# Create test DataFrame (simulating what serving endpoint sends)
test_df = pd.DataFrame([{
    &#8220;messages&#8221;: json.dumps([
        {&#8221;role&#8221;: &#8220;user&#8221;, &#8220;content&#8221;: &#8220;Say &#8216;Test Successful&#8217; and nothing else&#8221;}
    ]),
    &#8220;user_name&#8221;: &#8220;test.user@databricks.com&#8221;,
    &#8220;model&#8221;: &#8220;gpt-4.1-2025-04-14&#8221;,
    &#8220;max_tokens&#8221;: 50,
    &#8220;temperature&#8221;: 0.7
}])

print(&#8221;Test input DataFrame:&#8221;)
print(test_df)

model = TokenLimitedGatewayModel()
model.load_context(None)

print(&#8221;\nTesting with DataFrame input...&#8221;)
result = model.predict(None, test_df)
if &#8220;error&#8221; not in result:
    print(&#8221;Test successful!&#8221;)
    if &#8220;choices&#8221; in result:
        print(f&#8221;Response: {result[&#8217;choices&#8217;][0][&#8217;message&#8217;][&#8217;content&#8217;]}&#8221;)
    print(f&#8221;Usage info: {result.get(&#8217;usage_info&#8217;, {})}&#8221;)
else:
    print(f&#8221;Error: {result[&#8217;error&#8217;]}&#8221;)

# Log the model
with mlflow.start_run() as run:
    mlflow.pyfunc.log_model(
        artifact_path=&#8221;token_gateway&#8221;,
        python_model=TokenLimitedGatewayModel(),
        pip_requirements=pip_requirements,
        signature=signature
    )
    
    model_uri = f&#8221;runs:/{run.info.run_id}/token_gateway&#8221;
    print(f&#8221;Model logged with URI: {model_uri}&#8221;)
    print(f&#8221;Run ID: {run.info.run_id}&#8221;)

# Register to Unity Catalog
catalog = &#8220;&#8221;
schema = &#8220;&#8221; 
model_name = &#8220;token_limited_gateway&#8221;

registered_model = mlflow.register_model(
    model_uri=model_uri,
    name=f&#8221;{catalog}.{schema}.{model_name}&#8221;,
    tags={
        &#8220;use_case&#8221;: &#8220;rate_limiting&#8221;, 
        &#8220;model_type&#8221;: &#8220;gateway&#8221;,
        &#8220;backend&#8221;: &#8220;openai_gpt4&#8221;,
        &#8220;database&#8221;: &#8220;lakebase_postgres&#8221;,
        &#8220;version&#8221;: &#8220;dataframe_compatible&#8221;
    }
)</code></pre><p>All done.</p><p>Now we have another expense that can&#8217;t scale, adds latency, and is another component to maintain. </p><p>Or, we have a relatively low cost insurance policy against runaway token costs for employees we really wanted to enable on all our LLM endpoints but have previously been too worried about cost controls to do so. </p><p>Only you know which of these is the &#8220;correct&#8221; interpretation. My guess is both are right depending on your users and use case. It&#8217;s not perfect, but I said it was medieval right at the outset. </p><p>Cheers and happy coding!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Agents are like onions (they have layers)]]></title><description><![CDATA[Using custom scorers to investigate spans within a trace in MLflow 3+.]]></description><link>https://www.databricksters.com/p/agents-are-like-onions-they-have</link><guid isPermaLink="false">https://www.databricksters.com/p/agents-are-like-onions-they-have</guid><dc:creator><![CDATA[Veena]]></dc:creator><pubDate>Wed, 03 Sep 2025 16:27:33 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!6qcq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!6qcq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!6qcq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg 424w, https://substackcdn.com/image/fetch/$s_!6qcq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg 848w, https://substackcdn.com/image/fetch/$s_!6qcq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!6qcq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!6qcq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg" width="1280" height="720" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:720,&quot;width&quot;:1280,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:104376,&quot;alt&quot;:&quot;Shrek characters Object Detection Dataset by ML Labs&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Shrek characters Object Detection Dataset by ML Labs" title="Shrek characters Object Detection Dataset by ML Labs" srcset="https://substackcdn.com/image/fetch/$s_!6qcq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg 424w, https://substackcdn.com/image/fetch/$s_!6qcq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg 848w, https://substackcdn.com/image/fetch/$s_!6qcq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!6qcq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2efd0b1d-a602-4879-820a-52fc9c6d8148_1280x720.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Image of Shrek. He looks confused. </figcaption></figure></div><p>Agents are becoming more sophisticated. Traditionally, we use LLM judges to assess the quality of an Agent&#8217;s performance. Databricks has its <a href="https://docs.databricks.com/aws/en/mlflow3/genai/eval-monitor/predefined-judge-scorers#overview">own suite of pre-defined LLM scorers</a> that can evaluate safety, correctness, etc. For RAG agents, there are scorers that can help you analyze retrieval groundedness and retrieval relevance that can help analyze the quality of the retriever (aka the Vector Search). However, when the agent has access to multiple tools, end-to-end evaluation is not sufficient.</p><p>Imagine that we have an agent with access to several tools:</p><ul><li><p>A python code execution function</p></li><li><p>A retriever that gets relevant Databricks documentation</p></li><li><p>A translation function</p></li><li><p>A text summarization function</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!D10d!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!D10d!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png 424w, https://substackcdn.com/image/fetch/$s_!D10d!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png 848w, https://substackcdn.com/image/fetch/$s_!D10d!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png 1272w, https://substackcdn.com/image/fetch/$s_!D10d!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!D10d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png" width="728" height="924.2563580874872" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:false,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:1248,&quot;width&quot;:983,&quot;resizeWidth&quot;:728,&quot;bytes&quot;:101236,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:&quot;center&quot;,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!D10d!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png 424w, https://substackcdn.com/image/fetch/$s_!D10d!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png 848w, https://substackcdn.com/image/fetch/$s_!D10d!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png 1272w, https://substackcdn.com/image/fetch/$s_!D10d!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7d3d2ef5-0589-43ef-9402-95a672861a75_983x1248.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>As you can see in the diagram above, there are at minimum two LLM calls-- one when the agent begins and decides what tool to use (if there is a tool to be used) and another when to aggregate the tool output. How do we know if the LLM is making smart decisions about when to use these tools and what tools to use?</p><p>Consider these examples:</p><ul><li><p>Request: &#8220;What&#8217;s 2+2?&#8221;</p><ul><li><p>The agent should not use any tools, since this is quite simple math.</p></li></ul></li><li><p>Request: &#8220;What would <em>print(f&#8221;Random string {variable}&#8221;)</em> output in Python?&#8221;</p><ul><li><p>The agent should use the execute python function.</p></li></ul></li><li><p>Request: &#8220;How do I create a Databricks cluster?&#8221;</p><ul><li><p>The agent should use the retriever to get relevant docs.</p></li></ul></li></ul><p></p><p><a href="https://docs.databricks.com/aws/en/mlflow3/genai/tracing">With MLflow, we can generate traces easily. </a>In the example agent I am using here, I use langgraph and declare mlflow.langchain.autolog() to automatically trace every call in the app. Standard MLflow scorers would evaluate the final response quality, but they could miss whether the agent made smart tool choices. Custom scorers offer some flexibility here-- we can define one to break a trace down into its spans, independently analyzing each tool call.</p><h3>How do we implement a custom scorer?</h3><p>There are two main ways of implementing a custom scorer: (1) using the @scorer decorator for a python function or (2) using the Scorer class for more complex scorers that require state. In our example here, we are using the Scorer class and overriding the __call__ method.</p><p><a href="https://docs.databricks.com/aws/en/mlflow3/genai/eval-monitor/concepts/scorers">All scorers retriev</a>e the same inputs:</p><pre><code>  def __call__(
       self,
       *,
       inputs: Optional[dict[str, Any]],
       outputs: Optional[Any],
       expectations: Optional[dict[str, Any]],
       trace: Optional[mlflow.entities.Trace]
   ) -&gt; Feedback:</code></pre><p>First, we are going to determine the required tools. For each user input, the scorer uses an LLM judge to determine which tools should be required. MLflow offers two options for creating a judge: prompt based judges and guideline judges. <a href="https://docs.databricks.com/aws/en/mlflow3/genai/eval-monitor/concepts/judges/prompt-based-judge">Prompt based judges</a>, using the custom_prompt_judge function, allows to define custom categories (rather than pass/fail binaries) and have full prompt control.</p><p>Here, we are using a custom prompt and mapping our outputs (required and not required) to numeric values, which can be aggregated later on.</p><pre><code>from mlflow.genai.judges import custom_prompt_judge 

   def determine_required_tools(self, user_input: str, tools: Dict[str, str]) -&gt; Dict[str, bool]:
       required_tools = {}
       for tool_name, tool_description in tools.items():
           judge = custom_prompt_judge(
               name=f"{tool_name}_requirement_judge",
               prompt_template=tool_requirement_prompt,
               numeric_values={"required": 1.0, "not_required": 0.0}
           )
           result = judge(
               inputs=user_input,
               tool_name=tool_name,
               tool_description=tool_description
           )
           required_tools[tool_name] = result.value == 1.0
       return required_tools
</code></pre><p>Second, we are going to extract the actual behavior from the trace spans. MLflow automatically captures every tool call as a span with SpanType.TOOL. Our scorer searches through each span to get the tool name, tool response, and the tool status.</p><pre><code>  def extract_used_tools_from_trace(self, trace: mlflow.entities.Trace) -&gt; List[Dict[str, Any]]:
       tools = []
      
       spans = trace.search_spans(span_type=SpanType.TOOL)
       for span in spans:
           messages = span.get_attribute(SpanAttributeKey.OUTPUTS)
           content = json.loads(messages['content'])
           t = {"tool_call_id": messages['tool_call_id'],
               "tool_name": messages['name'],
               "tool_response": content['value'],
               "tool_status": messages['status']}
          
           tools.append(t)
       return tool</code></pre><p>This gives us more visibility into the agent&#8217;s decision-making process. Now that we have what tools were actually used, we can score this against what should have happened.</p><pre><code>  def compare_tool_usage(self, required_tools: Dict[str, bool], used_tools: List[Dict[str, Any]]) -&gt; Dict[str, Any]:
       required_tools = [name for name, required in required_tools.items() if required]
       correctly_used_tools = []
       failed_required_tools = []
       incorrectly_used_tools = []


       for tool in used_tools:
           if tool['tool_name'] in required_tools:
               correctly_used_tools.append(tool)
               required_tools.remove(tool['tool_name'])
&#9;&#9; # Custom logic to determine if tool execution was successful
               response = self.interpret_tool_call_response(tool['tool_name'], tool['tool_response'], tool['tool_status'])
               if response != "success":
                   failed_required_tools.append(tool)
           else:
               incorrectly_used_tools.append(tool)
              
       return {
           "correctly_used_tools": correctly_used_tools, # used and required ! :)
           "incorrectly_used_tools": incorrectly_used_tools, # used but not required
           "failed_required_tools": failed_required_tools, # required but response was not successful
           "missing_required_tools": required_tools # required but not used
       }</code></pre><p>Finally, we can compute the score based on the number of mistakes that the LLM made. The output of the Scorer should be a Feedback object. We can add rationale here as well, so when the scorer is used on new traces, we can look at the rationale directly in the MLflow Tracing UI.</p><p>The value of the Feedback object can be anything. Here we defined it as a simple Boolean, but it can be a Float, Int, String, a List, or a Dict. Again, lots of flexibility in how you define the LLM Scorers!</p><pre><code>           return Feedback(
               value=True,
               rationale="Used required tools properly. No feedback needed.",
               source=AssessmentSource(source_type="LLM_JUDGE", source_id="tool_usage_scorer")
               )


       else:
           return Feedback(
               value=False,
               rationale="Incorrectly used tools. Check metadata for more information. ",
               source=AssessmentSource(source_type="LLM_JUDGE", source_id="tool_usage_scorer")
               )</code></pre><p>You can finally use the scorer in your evaluation workflow.</p><h3>Key Takeaways</h3><p>By implementing custom scorers that analyze the spans within a trace, you can catch inappropriate LLM tool calls and usage and identify unnecessary tool usage. In order to use a custom scorer in MLflow:</p><ol><li><p>Define your evaluation criteria (what is good tool usage for your use case? Do you always want to be relying on tools?)</p></li><li><p>Create the custom scorer class</p></li><li><p>Integrate easily with the evaluation pipeline via mlflow.genai.evaluate()</p></li></ol><p>Practically, start tracking one or two critical tools rather than trying to evaluate everything all at once. In this example, I count every single mistake, but define what &#8216;too many mistakes&#8217; means in your use case. In your use case, you may not want to add everything except the failed tools and missing required tools.</p><p>Custom MLflow scorers give you the control to build more reliable agents. <a href="https://gist.github.com/veenaramesh/2c91cc8b8d0f7c688b4f238c5526b12c">Take a look at the full code implementation here.</a></p><p>Happy scoring!</p>]]></content:encoded></item><item><title><![CDATA[Doctors HATE this one dependency trick!]]></title><description><![CDATA[A quick guide to dependency management for machine learning using MLflow 3+.]]></description><link>https://www.databricksters.com/p/doctors-hate-this-one-dependency</link><guid isPermaLink="false">https://www.databricksters.com/p/doctors-hate-this-one-dependency</guid><dc:creator><![CDATA[Veena]]></dc:creator><pubDate>Tue, 19 Aug 2025 16:33:24 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!PGxA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Life was promised to be simple. You log your model and then you deploy the model. This is supposed to be easy! But even if everything works perfectly in your notebook, once you deploy it, dependency issues that you have never seen before pop up. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!PGxA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!PGxA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png 424w, https://substackcdn.com/image/fetch/$s_!PGxA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png 848w, https://substackcdn.com/image/fetch/$s_!PGxA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png 1272w, https://substackcdn.com/image/fetch/$s_!PGxA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!PGxA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png" width="988" height="575" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:575,&quot;width&quot;:988,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!PGxA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png 424w, https://substackcdn.com/image/fetch/$s_!PGxA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png 848w, https://substackcdn.com/image/fetch/$s_!PGxA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png 1272w, https://substackcdn.com/image/fetch/$s_!PGxA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F608e1ed6-1ca4-4e7b-acec-e70dca3d75b1_988x575.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Image showing what direct dependencies and transitive dependencies are. Direct dependencies are explicitly used in the project, and transitive dependencies are included as they are the dependencies of the direct dependencies.</figcaption></figure></div><p>Most dependency resolution issues are because of conflicting transitive dependencies. Here is what most data scientists are doing today: </p><ol><li><p>Experiment in notebooks, adding new libraries via pip within a notebook. </p></li><li><p>Log models using <code>mlflow</code> . </p></li><li><p>Register a model to UC and deploy. </p></li><li><p>Cross your fingers and hope everything works. </p></li><li><p>[optional] Get frustrated with Model Serving. </p></li></ol><p><code>mlflow</code> only infers the direct dependencies when you log a model. It identifies them by examining the flavor and the packages used in the model&#8217;s predict function. For more information, check out <code>mlflow.models.infer_pip_requirements()</code>. </p><p>Usually, this means that dependencies are only resolved during Model Serving, which means dependency errors are not caught until then. Waiting for the Serverless compute and the container to build will only extend the developer loop, making it harder to iterate. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!y6cU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!y6cU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png 424w, https://substackcdn.com/image/fetch/$s_!y6cU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png 848w, https://substackcdn.com/image/fetch/$s_!y6cU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png 1272w, https://substackcdn.com/image/fetch/$s_!y6cU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!y6cU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png" width="835" height="461" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:461,&quot;width&quot;:835,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!y6cU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png 424w, https://substackcdn.com/image/fetch/$s_!y6cU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png 848w, https://substackcdn.com/image/fetch/$s_!y6cU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png 1272w, https://substackcdn.com/image/fetch/$s_!y6cU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7989788f-c032-4858-a1d9-f29a4e5e1370_835x461.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Image showing that the requirements.txt file in the Registered Model artifacts are used to install all dependencies in the Model Serving environment. </figcaption></figure></div><h2>Lock your dependencies during development</h2><p>In <code>mlflow</code> 3+, you can enable dependency locking with <code>uv</code>, which would allow you to use the standard <code>mlflow</code> logging workflow. </p><pre><code><code>import os
os.environ["MLFLOW_LOCK_MODEL_DEPENDENCIES"] = "true"

# Now when you log your model, MLflow will capture 
# both direct AND transitive dependencies

mlflow.sklearn.log_model(
    model, 
    "my_model",
)</code></code></pre><p>Your workflow does not have to change at all. You can still use <code>extra_pip_requirements</code>, <code>pip_requirements</code>, or allow <code>mlflow</code> to infer all direct dependencies. The environment variable now enables <code>uv</code> to resolve dependencies during logging time and will capture pinned direct and transitive dependencies. Now, your <code>requirements.txt </code>file will contain all the dependencies you need. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!aHcq!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!aHcq!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!aHcq!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!aHcq!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!aHcq!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!aHcq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:216742,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/171375832?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!aHcq!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png 424w, https://substackcdn.com/image/fetch/$s_!aHcq!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png 848w, https://substackcdn.com/image/fetch/$s_!aHcq!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png 1272w, https://substackcdn.com/image/fetch/$s_!aHcq!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff3a9d835-1e25-4fc8-9f15-a7de811b5f34_1920x1080.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Image comparing the requirements.txt file artifact using dependency locking vs not using dependency locking. </figcaption></figure></div><p>Dependency resolution occurs during model logging time instead of serving time, and we have automatic dependency locking when logging a model. </p><p>However, since <code>uv</code> resolves all of the dependencies, the transitive dependencies captured are often more recent than the packages installed by default on the DBR. So, we will usually get &#8216;warnings&#8217; that the dependencies captured by <code>uv</code> are different from the transitive dependencies in the environment. Why is this a problem? Our training environment and serving environment are still different, which means we could still get behavior differences between our notebook and our deployment. </p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Q_rN!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Q_rN!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png 424w, https://substackcdn.com/image/fetch/$s_!Q_rN!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png 848w, https://substackcdn.com/image/fetch/$s_!Q_rN!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png 1272w, https://substackcdn.com/image/fetch/$s_!Q_rN!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Q_rN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png" width="1456" height="82" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:82,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:&quot;Screenshot 2025-07-28 at 5.44.57&#8239;PM.png&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="Screenshot 2025-07-28 at 5.44.57&#8239;PM.png" srcset="https://substackcdn.com/image/fetch/$s_!Q_rN!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png 424w, https://substackcdn.com/image/fetch/$s_!Q_rN!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png 848w, https://substackcdn.com/image/fetch/$s_!Q_rN!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png 1272w, https://substackcdn.com/image/fetch/$s_!Q_rN!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd7e97904-fb95-4e6f-9cc5-fff4a965b0e5_2048x115.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Example of a warning after using dependency locking: &#8220;Detected one or more mismatches between the model&#8217;s dependencies and the current Python environment: - cloudpickle (current: 2.2.1, required: cloudpickle==3.1.1)&#8221;</figcaption></figure></div><p>In the above example, we can see that <code>cloudpickle</code> often resolves to version 3.1.1, but in our recent DBRs, <code>cloudpickle</code> version 2.2.1 is installed. This is especially important because <code>cloudpickle</code> will always be a transitive dependency as <code>mlflow</code> relies on it. </p><h2>Using Databricks Asset Bundles</h2><p>We can resolve the inconsistency between the notebook environment and the serving environment using Databricks Asset Bundles. If we have a `dev` workspace and a `test` workspace, then we can use <code>mlflow</code> and <code>uv</code> to generate the requirements lock file in the `dev` workspace and add the requirements lock file as a dependency for the `test` workspace. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nohg!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nohg!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png 424w, https://substackcdn.com/image/fetch/$s_!nohg!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png 848w, https://substackcdn.com/image/fetch/$s_!nohg!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png 1272w, https://substackcdn.com/image/fetch/$s_!nohg!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nohg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png" width="301" height="644" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:644,&quot;width&quot;:301,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nohg!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png 424w, https://substackcdn.com/image/fetch/$s_!nohg!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png 848w, https://substackcdn.com/image/fetch/$s_!nohg!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png 1272w, https://substackcdn.com/image/fetch/$s_!nohg!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F1ca0da7d-6d0c-4864-b04c-a1fd8f1fbf40_301x644.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This can all be easily orchestrated using Databricks Asset Bundles and Lakeflow Jobs. By installing the requirements lock file, we can override any conflicting transitive dependencies in the DBR and ensure that the training and serving environments are the exact same. Here is an example job config: </p><pre><code><code>resources:
  jobs:
    my_job:
      tasks:
        - task_key: train_model
          libraries:
            - requirements: ./requirements.txt  # Pre-resolved dependencies</code></code></pre><p>Now the entire pipeline uses the same dependencies from development through production. </p><h2>Dependency management is important! </h2><p>Dependency management is probably not the most exciting part of MLOps but it can easily become a migraine. The few extra minutes you spend considering dependency management will save you lots of time debugging serving deployment failures. </p><p>As always, let us know if you have any questions! </p>]]></content:encoded></item><item><title><![CDATA[Beyond the Pipeline: The Blueprint for Enterprise AI Platforms using Databricks]]></title><description><![CDATA[Moving past dependency hell requires more than code&#8212;it demands a shift to a govern-first architecture.]]></description><link>https://www.databricksters.com/p/beyond-the-pipeline-the-blueprint</link><guid isPermaLink="false">https://www.databricksters.com/p/beyond-the-pipeline-the-blueprint</guid><dc:creator><![CDATA[Debu Sinha]]></dc:creator><pubDate>Tue, 05 Aug 2025 16:18:01 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/2798b17e-6012-48f8-8ee4-177f25f8f5df_2048x2048.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>As a Lead AI/ML Specialist Architect, I see a universal story unfold. It begins with the triumph of a single model, but as organizations scale, a predictable crisis emerges. I call it the <strong>Pipeline Paradox</strong>: as the number of models grows, the complexity of managing them grows exponentially, causing fragility, operational drag, and slowing innovation to a crawl.</p><p>This isn't a failure of talent; it's the result of hitting an architectural wall. The solution isn't a single tool or trick. It's a fundamental shift in perspective&#8212;from building individual, brittle pipelines to engineering a unified platform that embraces distinct, purpose-built architectural patterns. This is that blueprint.</p><h3>The Foundation: A Unified Governance Layer</h3><p>Before discussing execution, we must establish the non-negotiable foundation: a unified governance model. <strong>Unity Catalog</strong> provides this by treating all data and AI assets as first-class, governable citizens within a single system. It underpins every pattern below by providing a single source of truth, fine-grained permissions, automated end-to-end lineage, and streamlined CI/CD with model aliases (<code>@champion</code>).</p><h2>Prerequisites and Requirements</h2><p>To implement these patterns successfully, ensure:</p><ul><li><p><strong>Databricks Runtime:</strong> 15.4 LTS or above for ai_query function support.</p></li><li><p><strong>Compute Type:</strong> Serverless SQL warehouses for Patterns 1 &amp; 2, Spark clusters for Pattern 3.</p></li><li><p><strong>Unity Catalog:</strong> Enabled for governance and model management.</p></li><li><p><strong>Model Format:</strong> MLflow-packaged models registered in Unity Catalog.</p></li><li><p><strong>Permissions:</strong> `USE_FUNCTION` privilege on ai_query, appropriate model access grants.</p></li></ul><h3>The Three Core Model Inference Patterns on Databricks</h3><p>A mature AI platform on Databricks offers three distinct approaches for model inference, each optimized for different workloads. Understanding their trade-offs is key to choosing the right pattern for your use case.</p><h4>Pattern 1: Real-Time Model Serving Pattern</h4><p>This pattern is optimized for low-latency, request-response interactions. The primary tool is <strong>Databricks Model Serving</strong>, and it's accessed from SQL using <code>AI_QUERY</code> for ad-hoc analysis.</p><p>Here is what this looks like in practice for a data analyst performing a quick lookup:</p><p>SQL</p><pre><code><code>-- An ad-hoc query to get a churn prediction for a specific, high-value customer.
SELECT
  customer_id,
  -- ai_query calls the serving endpoint for a real-time response.
  ai_query(
    endpoint =&gt; 'prod_customer_churn_model', -- The name of your deployed model endpoint
    request =&gt; named_struct(  -- Pass features as a named struct
      'account_age', account_age,
      'monthly_spend', monthly_spend,
      'support_tickets', support_tickets
    ),
    returnType =&gt; 'DOUBLE'  -- Specify the return type for custom models
  ) AS churn_prediction_score
FROM
  main.gold.customer_features
WHERE
  customer_id = 'A-12345';</code></code></pre><h4>Pattern 2: Serverless Batch Inference Pattern</h4><p>This pattern is designed for maximum simplicity when applying a model to an entire dataset, using the same <code>AI_QUERY</code> function in a large-scale query.</p><p>In a SQL query, this pattern is strikingly simple:</p><p>SQL</p><pre><code><code>-- Enrich an entire customer table with churn scores using a single, scalable SQL statement.
-- Databricks optimizes this for batch performance using serverless compute.
CREATE OR REPLACE TABLE main.gold.customer_churn_predictions AS
SELECT
  customer_id,
  -- The same ai_query function, now applied to the whole table.
  ai_query(
    endpoint =&gt; 'prod_customer_churn_model',
    request =&gt; named_struct(
      'account_age', c.account_age,
      'monthly_spend', c.monthly_spend,
      'support_tickets', c.support_tickets
    ),
    returnType =&gt; 'DOUBLE'
  ) AS churn_prediction_score
FROM
  main.gold.customer_features AS c;
</code></code></pre><h4>Pattern 3: Embedded Spark UDF Pattern</h4><p>This pattern is engineered for maximum performance on the most demanding batch workloads, using <code>mlflow.pyfunc.spark_udf</code> to co-locate model execution with the data in Spark.</p><p>This is a more involved, code-first approach for ML engineering teams:</p><p>Python</p><pre><code><code>import mlflow
from pyspark.sql.functions import col, struct

# 1. Define the URI of the model in Unity Catalog.
model_uri = "models:/main.production_models.customer_churn/1"

# 2. Create the environment-aware Spark UDF.
#    'virtualenv' is faster for pure Python models.
#    For models with complex dependencies, use 'conda'.
predict_udf = mlflow.pyfunc.spark_udf(
    spark,
    model_uri=model_uri,
    env_manager="virtualenv",  # Use 'conda' for complex environments
    result_type="double"  # Specify return type for better performance
)

# 3. Read the source data.
features_df = spark.read.table("main.gold.customer_features")

# 4. Apply the UDF in a distributed fashion.
#    The model runs inside the Spark job, avoiding network calls.
predictions_df = features_df.withColumn(
    "churn_prediction_score",
    predict_udf(
        struct(col("account_age"), col("monthly_spend"), col("support_tickets"))
    )
)

# 5. Write the results to a new table.
predictions_df.write.mode("overwrite").saveAsTable("main.gold.customer_churn_predictions_udf")</code></code></pre><h3>The Architect's Blueprint: A Trade-off Analysis and Decision Framework</h3><p>Choosing the right pattern requires an honest assessment of what you are optimizing for.</p><h4>Analyzing the Patterns:</h4><ul><li><p><strong>Pattern 1 (Real-Time Model Serving):</strong> Optimizes for <strong>sub-second latency</strong> using Mosaic AI Model Serving endpoints. </p><ul><li><p><strong>Trade-off:</strong> Higher cost per prediction for bulk operations. </p></li><li><p><strong>Best for:</strong> Interactive applications, APIs, and real-time decision-making.</p></li></ul></li><li><p>  <strong>Pattern 2 (Serverless Batch Inference):</strong> Optimized for&nbsp;<strong>developer simplicity and automatic scaling,</strong>&nbsp;it offers 10-100x performance improvements (as of Dec 2024). </p><ul><li><p><strong>Trade-off:</strong> Network overhead between SQL warehouse and serving endpoint. </p></li><li><p><strong>Best for:</strong> Regular batch scoring, ETL pipelines, scheduled predictions.</p></li></ul></li><li><p> <strong>Pattern 3 (Embedded Spark UDF):</strong> Optimizes for <strong>maximum throughput and lowest cost</strong> by co-locating model execution with data. </p><ul><li><p><strong>Trade-off:</strong> Complex dependency management and potential version conflicts. </p></li><li><p><strong>Best for:</strong> Massive-scale batch processing, cost-sensitive workloads, models with simple dependencies.</p></li></ul></li></ul><h3>Conclusion: Making the Right Choice</h3><p>By starting with a foundation of governance in Unity Catalog and then using this trade-off analysis to select the right execution pattern, you can build a truly durable, scalable, and democratized engine for enterprise AI. The goal is not to find one pattern to rule them all, but to master the blueprint that lets you choose the right one, every time.</p>]]></content:encoded></item><item><title><![CDATA[It’s beaver time! Don’t get logged down with mlflow logging.]]></title><description><![CDATA[A simple workaround for when you are training thousands of models and log_model() becomes your worst enemy.]]></description><link>https://www.databricksters.com/p/its-beaver-time-dont-get-logged-down</link><guid isPermaLink="false">https://www.databricksters.com/p/its-beaver-time-dont-get-logged-down</guid><dc:creator><![CDATA[Veena]]></dc:creator><pubDate>Tue, 22 Jul 2025 16:02:23 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!49dG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!49dG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!49dG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg 424w, https://substackcdn.com/image/fetch/$s_!49dG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg 848w, https://substackcdn.com/image/fetch/$s_!49dG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!49dG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!49dG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg" width="612" height="408" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:408,&quot;width&quot;:612,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:92406,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/168898705?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!49dG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg 424w, https://substackcdn.com/image/fetch/$s_!49dG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg 848w, https://substackcdn.com/image/fetch/$s_!49dG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!49dG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F715ff9f7-3a3c-42c5-b9e5-4cd82ed89c32_612x408.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The beaver is building a dam. It is holding a log that says &#8216;mlflow.log_model()&#8217;.</figcaption></figure></div><p>Have you ever tried to train and log thousands of models with MLFlow? Instead of helping you track your runs, a seemingly innocuous line (<code>mlflow.log_model()</code>) has created a bottleneck and stretched your training time. Oh gosh, your training job takes hours now. What a nightmare.</p><h2>What does mlflow.log_model() do?</h2><p>Here is a brief overview of what log_model() does behind the scenes:</p><ol><li><p>Serializes the model.</p></li><li><p>Infers dependencies and manages manually added dependencies to create a requirements file.</p></li><li><p>Creates model specific assets (like MLmodel).</p></li><li><p>Handles metadata and versioning.</p></li></ol><p>All of these operations add a lot of overhead, and if you are looking to train thousands of small models within a single run, the logging overhead can often exceed your actual training time. This scenario is common in forecasting pipelines when you need separate models for different customer groups or time series segments.</p><h2>Instead, log these models to a Delta Table</h2><p>Let&#8217;s get a little creative. We can work around this issue by just not using log_model! We have solved the issue. You can stop reading now.</p><p>Just kidding. The idea is this-- we can get most of MLFlow&#8217;s benefits while improving performance by storing our models in a Delta Table.</p><pre><code><strong>Note</strong>: This is code to demonstrate the concept. Before using this code in production, please implement proper error handling and performance testing. Take a look at the <a href="https://gist.github.com/veenaramesh/a7eed4f2fa8ac3386f34b8c22a442602">entire notebook here.</a></code></pre><p>First, we need to create a Delta Table.</p><pre><code>schema = StructType([
    StructField("group_id", StringType(), True),
    StructField("model_type", StringType(), True),
    StructField("model_version", StringType(), True),  
    StructField("model_binary", BinaryType(), True),
    StructField("run_id", StringType(), True),
    StructField("run_date", TimestampType(), True),
    StructField("mse", DoubleType(), True),
    StructField("forecast", ArrayType(DoubleType()), True),
    StructField("actual", ArrayType(DoubleType()), True),
    StructField("is_latest", StringType(), True)  
])

spark.createDataFrame([], schema).write.format("delta").option("overwriteSchema", "true").saveAsTable("&lt;CATALOG&gt;.&lt;SCHEMA&gt;.mlflow_runs")</code></pre><p>In this example, I use the same Delta Table to capture all of my models across different runs. I do this so I can implement a model_version column that will capture the latest version of each model. However, if you do not need this information, you can create a new Delta Table for each MLFlow run. Another thing to note is that I am using one type of model (a Random Forest regressor), but you can imagine a world where you have multiple different model types as well. This workaround is easily extendable to a lot of these scenarios.</p><p>When I train the model, I capture all of the information I need, like the predictions, actuals, metrics (here, I am using mean squared error). I am also capturing useful information like group_id, run_id, model_type to make it easy for me to search for this specific model after it is logged to the Delta Table. Finally, I have dumped the model binary in as well.</p><pre><code>def train_model(group_df, group_id, latest_model_version, run_id, run_date): 
    ...
    # train the model as usual 
    ...

    # return metadata 
    return {
        'group_id': group_id,
        'model_type': 'RandomForestRegressor',
        'model_version': str(latest_model_version + 1),
        'model_binary': cloudpickle.dumps(model), 
        'run_id': run_id,
        'run_date': run_date,
        'mse': mse,
        'forecast': predictions.tolist(),
        'actual': y.tolist(),
        'is_latest': "True"
    }</code></pre><p>After training for all models is complete, I can batch-insert all of the models into the Delta Table in one operation:</p><pre><code>def save_to_delta(model_results, table_name): 
  df = spark.createDataFrame(model_results)
  df.write.format("delta").mode("append").saveAsTable(table_name)
  return 

...
# in the mlflow run
...

for group_id in GROUP_IDS: 
  group_df = data[data['group_id'] == group_id] 

  latest_version = current_model_versions.get(group_id, 0)
  model_result = train_model(group_df, group_id, latest_version, run_id, run_date)
  all_model_results.append(model_result)

save_to_delta(all_model_results, table_name)</code></pre><p>Depending on how many models you are training, you can also update the Delta Table with the model information immediately after training.</p><p>Even though we are storing models in Delta Tables, we still want to maintain the link back to the MLFlow run for reproducibility. We want to log high-level information like, the number of models trained, the training dataset (assuming it's the same across groups), and the Delta Table used for logging. MLFlow also automatically logs other important information, like start-end times, success-failure statistics, and the source notebook version or git commit associated with the run. All of this is incredibly important for reproducibility and auditing. This is why we log the run_id with the model-- we want to be able to cross reference between the Delta Table and the MLFlow experiment to get the best of both worlds.</p><p>But if you recall the beginning of this post, you would remember there is still one thing left to cover: tracking dependencies. Without log_model(), MLFlow does not infer dependencies or create the MLModel artifacts, which contains important information such as the Python version used. After saving all of the models to a Delta Table, we can use log_model() once to create these artifacts. Now, we can save all of the required dependencies.</p><pre><code>def log_models_to_mlflow(data, table_name="&lt;CATALOG&gt;.&lt;SCHEMA&gt;.mlflow_runs"): 
  with mlflow.start_run() as run: 
    run_id = run.info.run_id
    run_date = datetime.now()

    # log high level parameters
    mlflow.log_param("num_groups", len(data['group_id'].unique()))
    mlflow.log_param("delta_table_name", table_name)

    current_model_versions = get_latest_model_versions(table_name) # {'group_id': version #}
    all_model_results = []

    for group_id in GROUP_IDS: 
      group_df = data[data['group_id'] == group_id] 

      latest_version = current_model_versions.get(group_id, 0)
      model_result = train_model(group_df, group_id, latest_version, run_id, run_date)
      all_model_results.append(model_result)

    save_to_delta(all_model_results, table_name)

    mlflow.pyfunc.log_model(
        "dummy_model",
        input_example=main_df,
&#9; extra_pip_requirements=[...], 
        python_model=DummyWrapper()
    )</code></pre><p>You can also do this by logging a requirements.txt file directly via <a href="https://gist.github.com/veenaramesh/a7eed4f2fa8ac3386f34b8c22a442602">mlflow.log_artifact().</a></p><h2>But how do we load these models?</h2><p>It is quite straightforward. Since we have saved the model binaries in the Delta Table, we can directly load this and use it for inference. In this example, remember I have saved all of the models across runs, so I have multiple versions of each model in the same Delta Table. You can easily search and get specific versions of the model or simply retrieve the latest one.</p><pre><code>class MultiModelWrapper(): 
  def __init__(self, table_name): 
    self.table = table_name

  def load_model_from_delta(self, group_id, table_name, model_type=None, run_id=None, version=None): 
    query = f"select * from {table_name} where group_id = '{group_id}'"
    if model_type: 
      query += f" and model_type = '{model_type}'"
    if run_id: 
      query += f" and run_id = '{run_id}'"
    if version: 
      query += f" and model_version = '{version}'"
    else: 
      query += f" and is_latest = 'True'" 
    model_df = spark.sql(query).collect()
    if model_df: 
      model = cloudpickle.loads(model_df[0]['model_binary'])
      metadata = model_df[0].asDict(True)
      metadata.pop("model_binary")
      return model, metadata
    else: 
      return None, None
    
  def predict(self, model_input, group_id, model_type=None, run_id=None, version=None): 
    model, _ = self.load_model_from_delta(group_id=group_id, model_type=model_type, run_id=run_id, version=version, table_name=self.table)

    # TODO: make sure the model_input can be ingested by the models!
    return model.predict(model_input.values)

wrapper_model = MultiModelWrapper(table_name="&lt;CATALOG&gt;.&lt;SCHEMA&gt;.mlflow_runs")
test_df = main_df.head(1).drop(columns=["group_id", "target", "date"])
wrapper_model.predict(test_df, 'A', version=2)</code></pre><h2>When does this approach make sense for me?</h2><p>If you have many (large hundreds to thousands) small models that are similar and are seeing lag in model training using MLFlow, I would suggest looking into this workaround. If you are not frequently training these models or the models are not easily serializable (such as deep learning models), I would not recommend this solution for you.</p><p>Obviously, you lose a lot of native MLflow features, like connection to the UC Model Registry, and there is a lot of custom code to maintain, but for high volume scenarios, these trade-offs are usually worth it.</p><p>This pattern can offer your team a pragmatic solution that maintains most of MLFlow&#8217;s tracking benefits while improving performance for bulk model training.</p><p>I hope this guide was helpful! Please let me know if you have any questions below. <a href="https://gist.github.com/veenaramesh/a7eed4f2fa8ac3386f34b8c22a442602">Again, here is the link to the notebook. </a></p>]]></content:encoded></item><item><title><![CDATA[Juggling multiple models in a single serving endpoint]]></title><description><![CDATA[How to serve multiple models on a single model serving endpoint in Databricks using pyfunc]]></description><link>https://www.databricksters.com/p/juggling-a-model-circus-a-pyfuncs</link><guid isPermaLink="false">https://www.databricksters.com/p/juggling-a-model-circus-a-pyfuncs</guid><dc:creator><![CDATA[Veena]]></dc:creator><pubDate>Tue, 29 Apr 2025 16:02:04 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!n1Ap!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever found yourself juggling multiple ML models? Imagine this: you're maintaining a prediction service that started with a single model, but now you've got a dozen micro-models serving different business needs. Costs are climbing. You are dreaming of consolidation.</p><p>For most scenarios, Databricks Model Serving provides an easy solution. They allow you to deploy multiple models behind a single endpoint, split traffic, and route requests. This approach is perfect for A/B testing and canary deployments, where simple traffic splitting is sufficient. However, there are situations where we can hit limitations:  </p><ul><li><p>routing based on requests (e.g., user attributes) </p></li><li><p>routing based on time</p></li><li><p>managing dozens of micro-models and want to consolidate infrastructure</p></li><li><p>routing dynamically based on business rules</p></li></ul><p>You could spin up separate endpoints for each, but that means more DBUs, more management overhead, etc. This is where creating a custom PyFunc wrapper can provide a solution. Note that this should be viewed as an edge case and not a default pattern. </p><h3>Before diving into the implementation, let&#8217;s consider the limitations. </h3><ul><li><p>Individual model metrics are combined, so monitoring is more difficult.</p></li><li><p>Models are loaded together, so there could be a resource inefficiency.</p></li><li><p>Routing rules may obscure decision paths. </p></li><li><p>Model versioning is less transparent. </p></li></ul><p>In this deep dive, we will explore a really simple pattern to help solve this issue using PyFunc. By creating a wrapper with PyFunc, we will package various models in one deployable artifact, implement routing logic to direct requests to the right model, and maintain an entry point. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!n1Ap!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!n1Ap!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png 424w, https://substackcdn.com/image/fetch/$s_!n1Ap!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png 848w, https://substackcdn.com/image/fetch/$s_!n1Ap!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png 1272w, https://substackcdn.com/image/fetch/$s_!n1Ap!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!n1Ap!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png" width="1456" height="591" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:591,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:183561,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/162350351?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!n1Ap!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png 424w, https://substackcdn.com/image/fetch/$s_!n1Ap!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png 848w, https://substackcdn.com/image/fetch/$s_!n1Ap!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png 1272w, https://substackcdn.com/image/fetch/$s_!n1Ap!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fff7b1f14-54a4-46fa-aa52-1a9c7f7ff468_3614x1466.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">An diagram of how this router solution would look. </figcaption></figure></div><h3>Let&#8217;s quickly create some base models. </h3><p>We are training two separate models using the same California Housing dataset. Because both models have the exact same input data schema and the expected output schema, we can expect that the Model Signature for both models will be the same. </p><pre><code>import mlflow
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor

data = fetch_california_housing()
california_housing = pd.DataFrame(data.data, columns=data.feature_names)
california_housing['target'] = data.target

X_train, X_test, y_train, y_test = train_test_split(
    california_housing.drop('target', axis=1), 
    california_housing['target'], 
    test_size=0.2, 
    random_state=42
)

lr_model = LinearRegression().fit(X_train, y_train)
rf_model = RandomForestRegressor().fit(X_train, y_train)

signature = mlflow.models.infer_signature(X_train, lr_model.predict(X_train))</code></pre><p>This represents a standard model development workflow. This pattern builds on existing models and training processes rather than replacing them. In other words, you can adopt this pattern without too much disruption to your current workflows. </p><p>These can now be logged and registered in Unity Catalog. Nothing new here!  </p><pre><code>with mlflow.start_run(run_name="California Housing Models") as housing_run:
    mlflow.sklearn.log_model(lr_model, "linear_regression_model", signature=signature)
    mlflow.sklearn.log_model(rf_model, "random_forest_model", signature=signature)
    
    mlflow.set_registry_uri("databricks-uc")
    mlflow.register_model(
        f"runs:/{housing_run.info.run_id}/linear_regression_model", 
        "your_catalog.your_schema.california_housing_linear_regression"
    )
    mlflow.register_model(
        f"runs:/{housing_run.info.run_id}/random_forest_model", 
        "your_catalog.your_schema.california_housing_random_forest"
    )
</code></pre><h3>Create a custom model using pyfunc. </h3><p>We are going to use <code>pyfunc</code> to orchestrate and serve as the main interface for interacting with the base models. The wrapper will load our models and dynamically select which model to use based on the request parameters. </p><pre><code>class ModelRouter(mlflow.pyfunc.PythonModel):
    def load_context(self, context):
        self.linear_model = mlflow.sklearn.load_model(
            context.artifacts["linear_regression_model"]
        )
        self.forest_model = mlflow.sklearn.load_model(
            context.artifacts["random_forest_model"]
        )

    def predict(self, context, model_input):
        # The 'model' column specifies which model to use
        if model_input['model'].eq('RandomForest').any():
            return {
                "prediction": self.forest_model.predict(model_input.drop('model', axis=1))
            }
        elif model_input['model'].eq('LinearRegression').any():
            return {
                "prediction": self.linear_model.predict(model_input.drop('model', axis=1))
            }
        else:
            raise ValueError("Unrecognized model type. Use 'RandomForest' or 'LinearRegression'")</code></pre><p>I want to highlight two important aspects of this wrapper. First, in <code>load_context</code>, we are loading the underlying Linear Regression and Random Forest models from the artifacts.  When we log and register this wrapper, we will need to specify these artifacts, so that the wrapper will correctly load the models that we trained. Keep in mind that in the model serving environment, <code>load_context</code> is called once, so loading the models should not affect the serving latency after initialization. </p><p>Second, there is a lot of flexibility here. In the code snippet, we are using an extra column in the model input called <code>model</code> to select which model to use. But you can implement virtually any routing logic. You can switch between the models based on geographic location or the time the request was submitted.</p><h3>Registering the wrapper with the model artifacts. </h3><p>In order to register the model, we need to create a proper Model Signature. I am going to use the <code>infer_signature</code> function to do so. You can also manually construct the signature object. The signature will be similar to the signatures used for the base models. Because our wrapper uses an extra column to decide which model to use, we need to take that into consideration. </p><pre><code>input_example = X_train.copy()
input_example['model'] = 'RandomForest'

router_signature = mlflow.models.infer_signature(
    input_example, 
    {"prediction": rf_model.predict(X_train)}
)</code></pre><p>When we log the model, we need to include the base models as artifacts: </p><pre><code>with mlflow.start_run() as run:
    router_model = ModelRouter()
    mlflow.pyfunc.log_model(
        "model_router",
        python_model=router_model,
        signature=router_signature,
        artifacts={
            "linear_regression_model": 
                "models:/your_catalog.your_schema.california_housing_linear_regression/1",
            "random_forest_model": 
                "models:/your_catalog.your_schema.california_housing_random_forest/1",
        },
        extra_pip_requirements=["scikit-learn==1.4.2", "numpy==1.23.5", "pandas==1.5.3"]
    )
    
    # Register the router model
    mlflow.register_model(
        f"runs:/{run.info.run_id}/model_router", 
        "your_catalog.your_schema.housing_model_router"
    )</code></pre><p>Now, we have created a self-contained wrapper that includes everything needed for serving. </p><h2>What happens when we are dealing with different inputs?</h2><p>Imagine your system spans multiple domains. Different data, different tasks, but you still need a unified interface. </p><p>First, let&#8217;s train model on a different dataset.  </p><pre><code>from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier

cancer_data = load_breast_cancer()
cancer_df = pd.DataFrame(cancer_data.data, columns=cancer_data.feature_names)
cancer_df['target'] = cancer_data.target

X_train_cancer, X_test_cancer, y_train_cancer, y_test_cancer = train_test_split(
    cancer_df.drop('target', axis=1), 
    cancer_df['target'], 
    test_size=0.2
)

cancer_model = RandomForestClassifier().fit(X_train_cancer, y_train_cancer)

with mlflow.start_run() as run:
    cancer_signature = mlflow.models.infer_signature(
        X_train_cancer, 
        cancer_model.predict(X_train_cancer)
    )
    mlflow.sklearn.log_model(
        cancer_model, 
        "random_forest_cancer", 
        signature=cancer_signature
    )
    
    mlflow.register_model(
        f"runs:/{run.info.run_id}/random_forest_cancer", 
        f"{catalog}.{db}.{rf_br_model_name}"
    )</code></pre><p>Now, we can create a wrapper that handles both datasets. This wrapper is similar to the previous one. </p><pre><code>class MultiDomainRouter(mlflow.pyfunc.PythonModel):
    def load_context(self, context):
        self.housing_model = mlflow.sklearn.load_model(
            context.artifacts["housing_model"]
        )
        self.cancer_model = mlflow.sklearn.load_model(
            context.artifacts["cancer_model"]
        )
        
        self.housing_columns = context.artifacts['housing_features']
        self.cancer_columns = context.artifacts['breast_cancer_features']

    def predict(self, context, model_input):
        if model_input['domain'].eq('housing').any():
            # validate input data
            input_cols = set(model_input.columns) - {'domain'}
            missing_cols = self.housing_columns - input_cols
            if missing_cols:
                raise ValueError(f"Missing required columns for model: {missing_cols}")
                
            # columns needed by  model
            features = model_input[list(self.housing_columns) + ['domain']]
            return {
                "prediction": self.housing_model.predict(
                    features.drop('domain', axis=1)
                )
            }
            
        elif model_input['domain'].eq('cancer').any():
            # validate input data
            input_cols = set(model_input.columns) - {'domain'}
            missing_cols = self.cancer_columns - input_cols
            if missing_cols:
                raise ValueError(f"Missing required columns for model: {missing_cols}")
                
            # columns needed by model
            features = model_input[list(self.cancer_columns) + ['domain']]
            return {
                "prediction": self.cancer_model.predict(
                    features.drop('domain', axis=1)
                )
            }
        else:
            raise ValueError("Unrecognized domain. Use 'housing' or 'cancer'")</code></pre><p>But wait&#8212; how are we supposed to define the Model Signature? The expected inputs will be different. </p><h4>A quick aside on Model Signatures. </h4><p>A Model Signature defines the input and output schema that the model is expected to receive and output. There are two main types of signatures: column-based (used for most traditional ML models) and tensor-based (used for deep learning applications). </p><p>Column-based signatures consist of a list of columns (very surprising), each with an expected data type. The signature for the California Housing models look like this: </p><pre><code>inputs: 
  ['MedInc': double (required), 'HouseAge': double (required), 'AveRooms': double (required), 'AveBedrms': double (required), 'Population': double (required), 'AveOccup': double (required), 'Latitude': double (required), 'Longitude': double (required)]
outputs: 
  [Tensor('float64', (-1,))]</code></pre><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!MC11!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!MC11!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png 424w, https://substackcdn.com/image/fetch/$s_!MC11!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png 848w, https://substackcdn.com/image/fetch/$s_!MC11!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png 1272w, https://substackcdn.com/image/fetch/$s_!MC11!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!MC11!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png" width="1456" height="1010" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1010,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:301350,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/162350351?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!MC11!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png 424w, https://substackcdn.com/image/fetch/$s_!MC11!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png 848w, https://substackcdn.com/image/fetch/$s_!MC11!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png 1272w, https://substackcdn.com/image/fetch/$s_!MC11!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8c441e9d-5a0c-47c3-86e3-222a6e1a6526_1872x1298.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The function `mlflow.models.infer_signature()` automatically takes an existing dataset and creates a schema with the appropriate datatypes. </figcaption></figure></div><p>Notice: each of these columns are required by the model. Required fields must be included in the input, and if it is not there, it will error out. However, we can include optional fields as well. </p><p>The function assumes that these columns are required because in the dataframe I used, all values were properly populated. In order to configure a field as optional, we can use <code>mlflow.models.infer_signature</code> by passing in some <code>None</code> values for that field. Basically, we can concat the two datasets and infer the signature to make all of the columns for both types of inputs optional. </p><pre><code>bc_head = bc.head()
california_housing_head = california_housing.head()

merged_df = pd.concat([bc_head, california_housing_head])
merged_df_output =  {"california_housing": rf_ch.predict(california_housing_head.drop('target', axis=1))}
signature_merged_df = infer_signature(merged_df, merged_df_output)</code></pre><p>By making most fields optional in the signature, we're essentially telling MLflow to let all requests through to our code. We will still need to do the validation for each model. This gives us the flexibility to route between completely different models while still maintaining a consistent interface for client applications.</p><p>If you look at the code snippet where we defined the model wrapper again, you can see that we have performed the validation of the model inputs ourselves. We have manually added feature names that are required for each model to enforce the input schema. </p><h1>Conclusion</h1><p>Voil&#224;! This approach should address more complex needs to routing logic and consolidation of small models. It is perfect when you need: </p><ol><li><p>request-level routing beyond percentage-based traffic splitting</p></li><li><p>multiple small models where separate endpoints would be inefficient. </p></li><li><p>complex routing logic based on request properties. </p></li></ol><p>This router pattern gives you flexibility way to consolidate multiple models behind a single endpoint. Remember to think about the limitations we listed out earlier before implementing this in production!</p><p>Happy wrapping!</p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[PyFunc it! We'll do it Live! ]]></title><description><![CDATA[Real-Time Data Preprocessing for Custom Databricks Model Serving Endpoints]]></description><link>https://www.databricksters.com/p/pyfunc-it-well-do-it-live</link><guid isPermaLink="false">https://www.databricksters.com/p/pyfunc-it-well-do-it-live</guid><dc:creator><![CDATA[Austin]]></dc:creator><pubDate>Tue, 22 Apr 2025 16:53:56 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/dcf6be7d-3d5d-475a-9bad-c168ed784065_300x256.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When performing real time inference, you rarely get all of the data needed to make your prediction exactly how your model requires it inside the <code>POST</code> request. More commonly, one or both of the following are true:</p><ol><li><p>The data received requires significant preprocessing in the form of parsing, encoding, reformatting, etc.</p></li><li><p>The data received is incomplete and must be combined with another data set in order to perform accurate predictions</p></li></ol><p>Today&#8217;s blog will focus on the first use case, and we will revisit the second one next quarter. I originally wrote Part 2 using <a href="https://docs.databricks.com/aws/en/machine-learning/feature-store/online-tables">Online Tables</a>, which is still a possibility, but there have been some API changes I want to make sure settle before publishing. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!z2vS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!z2vS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif 424w, https://substackcdn.com/image/fetch/$s_!z2vS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif 848w, https://substackcdn.com/image/fetch/$s_!z2vS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif 1272w, https://substackcdn.com/image/fetch/$s_!z2vS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!z2vS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif" width="320" height="273.06666666666666" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/9a560027-e022-413f-a218-6de1d788afb1_300x256.gif&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:256,&quot;width&quot;:300,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:4742917,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/gif&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/161897285?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!z2vS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif 424w, https://substackcdn.com/image/fetch/$s_!z2vS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif 848w, https://substackcdn.com/image/fetch/$s_!z2vS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif 1272w, https://substackcdn.com/image/fetch/$s_!z2vS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9a560027-e022-413f-a218-6de1d788afb1_300x256.gif 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Bill gets frustrated with pre-processing pipelines</figcaption></figure></div><h3><strong>The Power of Pipelines</strong></h3><p>If we have an <code>sklearn</code> model, adding preprocessing steps - even more advanced custom preprocessing classes - is a straightforward task:</p><pre><code>import mlflow
import mlflow.sklearn
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.datasets import load_iris

# Yes this dataset is simplistic, but we're just proving a point right now
iris = load_iris()
X = iris.data
y = iris.target

# Create the pipeline with whatever preprocessing you may need, Pipeline also accepts custom classes
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('model', LogisticRegression())
])

# Start the MLflow run with autologging for even more quality of life features
with mlflow.start_run():
    mlflow.sklearn.autolog()
    pipeline.fit(X, y)

mlflow.end_run()</code></pre><p>Not exactly groundbreaking code here. But what if our custom parsing and preprocessing logic was <strong>really</strong> complex and we wanted to maintain our own separate python scripts for this logic to maintain modularity? What if we wanted to use a model type that doesn't fit nicely into <code>sklearn</code>? Regardless of the specific motivation, the time may come when this pattern will no longer serve our needs. Calling a separate <code>.py</code> file from within a <a href="https://mlflow.org/docs/latest/traditional-ml/creating-custom-pyfunc/part2-pyfunc-components.html">PyFunc</a> Python model and serving the custom pipeline is an extremely powerful and flexible pattern for multipart inference pipelines. </p><p><strong>Note:</strong><em> You can still use an </em><code>sklearn</code><em> Pipeline for this without using the </em><code>mlflow.sklearn</code><em> flavor, because </em><code>XGBoost</code><em> provides an </em><code>sklearn</code><em> compatible API. I've run the code below both ways, and while you don't have to use a single </em><code>sklearn</code><em> package in order to leverage this pattern, it will make for a simpler to follow demo.</em></p><h2><strong>Defining a Custom Preprocessing Script</strong></h2><p>The below <code>.py</code> file contains two relatively simple preprocessing classes, one that flattens nested JSON strings and one that extracts the domain from email addresses.</p><pre><code>## custom_transformers.py
## We could break these out, but for simplicity of the demo, I'm just making one external .py file
from sklearn.base import BaseEstimator, TransformerMixin
import pandas as pd

class JSONFlattener(BaseEstimator, TransformerMixin):
    """
    Transforms the DataFrame by flattening the specified JSON column into a tabular format.
    """
    def __init__(self, json_column, record_prefix=''):
        self.json_column = json_column
        self.record_prefix = record_prefix

    def fit(self, X, y=None):
        return self

    def flatten_dict(self, d, parent_key='', sep='.'):
        items = []
        for k, v in d.items():
            new_key = f"{parent_key}{sep}{k}" if parent_key else k
            if isinstance(v, dict):
                items.extend(self.flatten_dict(v, new_key, sep=sep).items())
            else:
                if isinstance(v, list):
                    v = ';'.join(map(str, v))
                items.append((new_key, v))
        return dict(items)

    def transform(self, X):
        X = X.copy()
        flattened = X[self.json_column].apply(lambda x: self.flatten_dict(x, self.record_prefix, sep='.'))
        json_df = pd.DataFrame(flattened.tolist())
        X = X.drop(columns=[self.json_column])
        X = pd.concat([X.reset_index(drop=True), json_df.reset_index(drop=True)], axis=1)
        return X

class EmailDomainExtractor(BaseEstimator, TransformerMixin):
    """
    Transforms the DataFrame by adding a new column 'email_domain' containing the extracted domains.
    """
    def __init__(self, email_column):
        self.email_column = email_column

    def fit(self, X, y=None):
        return self

    def transform(self, X):
        X = X.copy()
        if self.email_column not in X.columns:
            raise ValueError(f"Column '{{self.email_column}}' not found in input data.")
        X['email_domain'] = X[self.email_column].apply(
            lambda x: x.split('@')[-1] if isinstance(x, str) and '@' in x else 'unknown'
        )
        return X</code></pre><p>Being a fully self-specified data format, JSON strings are an extremely popular method for sending data over the internet, and in order to suit the wide variety of use cases that leverage JSON, they can get quite complex. <a href="https://docs.databricks.com/en/machine-learning/model-serving/index.html">Databricks model serving</a> limits the payload of an individual request to <a href="https://docs.databricks.com/en/machine-learning/model-serving/model-serving-limits.html#resource-and-payload-limits">16MB</a>, which a single JSON could hypothetically occupy all of. Needless to say, our two level JSON flattener class is only meant to be a placeholder to showcase a broader pattern.</p><p>Now that we have our <code>.py</code> file defined, let's open a notebook in the same folder and turn our cluster on if we don't already have one. For this code I used an <code>r6i.xlarge</code> single node CPU cluster on Databricks ML Runtime 15.4 LTS on AWS, but a rough equivalent in Azure would be the <code>E4s_v4</code>, since both feature 4 vCPUs with 8 GiB of RAM, each powered by Intel Xeon Ice Lake processors. These are both fast and low cost, with the AWS one coming in at 1.02 DBU/hr.</p><h2><strong>Synthetic Data Generation</strong></h2><p>To keep the code in this blog fully functional out of the box, we're going to generate some synthetic data using one of my favorite python packages, <code>Faker</code>.</p><pre><code># Note: as we said above, this pattern is not dependent on sklearn Pipelines
%pip install faker==18.11.2
%pip install scikit-learn==1.2.2
%pip install databricks-sdk --upgrade
%pip install mlflow==2.17.0
dbutils.library.restartPython()</code></pre><pre><code># We'll use a non-sklearn ML package for our main model, XGBoost
import pandas as pd
import numpy as np
from faker import Faker
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.metrics import classification_report
import mlflow
import mlflow.pyfunc
from mlflow.models.signature import infer_signature
import xgboost as xgb
import joblib
import os
import sys</code></pre><p>We'll generate 1,000 rows for now, but you could generate far more if you wanted to experiment with the scalability of your model.</p><pre><code># Faker makes synthetic data generation easy
fake = Faker()
Faker.seed(42)
np.random.seed(42)

def generate_data(num_rows=1000):
    data = []
    for _ in range(num_rows):
        customer_id = fake.unique.uuid4()
        name = fake.name()
        address = {
            'street': fake.street_address(),
            'city': fake.city(),
            'state': fake.state_abbr(),
            'zip_code': fake.zipcode()
        }
        email = fake.email()
        phone_number = fake.phone_number()
        
        transaction = {
            'transaction_id': fake.unique.uuid4(),
            'amount': round(np.random.uniform(10.0, 1000.0), 2),
            'transaction_type': np.random.choice(['online', 'in-store', 'cash withdrawal', 'mobile']),
            'account_age_days': np.random.randint(30, 3650),
            'customer_info': {
                'customer_id': customer_id,
                'name': name,
                'address': address,
                'email': email,
                'phone_number': phone_number
            },
            'fraud': np.random.choice([0, 1], p=[0.95, 0.05])
        }
        data.append(transaction)
    return pd.DataFrame(data)

df = generate_data(num_rows=1000)
display(df.head(5))</code></pre><div><hr></div><h2><strong>Short aside: What if my preprocessing logic are other models?</strong></h2><p>If you had additional ML models in your pipeline, there are a few ways you could incorporate them together depending on the flow pattern of the data. For example, if you have multiple <strong>independent</strong> processes, then you can use a fan out model where models can process in parallel on their own endpoints, then send those results back to a wrapper model for the final prediction. The pros of this are that you can scale multiple models independently, and if you have multiple heavily duty models, this may be the fastest way to structure things. The cons are that you have more cost, both in terms DBUs (since you have multiple running endpoints) and in terms of overhead latency, since each call from one endpoint to another is going to add ~50ms.</p><p>The other option, especially if you have multiple <strong>dependent</strong> processes, is to wrap the constituent models up in the orchestrator as model artifacts and serve the entire pipeline to a single endpoint. The pros of this are that you save on costs, financially and in overhead latency, and since your model processes are dependent anyway, there is no efficiency loss due to models that could be run in parallel laying idle. The cons are that you need to be more careful in the initial deployment since it's less modularized, but I have some tips to share on that below.</p><p>Note that your preprocessing logic being other models does not fundamentally change anything, since a PyFunc model can be really <strong>any</strong> executable python code that takes <code>X</code> and returns <code>y</code>, you could even host your non-model preprocessing logic on separate endpoints if you wanted to. However, it would be rather unconventional and I can't contrive a scenario where that would be advantageous at the moment.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!tYWV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!tYWV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png 424w, https://substackcdn.com/image/fetch/$s_!tYWV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png 848w, https://substackcdn.com/image/fetch/$s_!tYWV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png 1272w, https://substackcdn.com/image/fetch/$s_!tYWV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!tYWV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png" width="1456" height="694" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:694,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:44621,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/161897285?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!tYWV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png 424w, https://substackcdn.com/image/fetch/$s_!tYWV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png 848w, https://substackcdn.com/image/fetch/$s_!tYWV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png 1272w, https://substackcdn.com/image/fetch/$s_!tYWV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F15ed3fce-389e-456e-b9d1-fb646a275353_1920x915.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><strong>Multiple Endpoints Version</strong></figcaption></figure></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1eBd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1eBd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png 424w, https://substackcdn.com/image/fetch/$s_!1eBd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png 848w, https://substackcdn.com/image/fetch/$s_!1eBd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png 1272w, https://substackcdn.com/image/fetch/$s_!1eBd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1eBd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png" width="1456" height="810" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:810,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:47634,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/161897285?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1eBd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png 424w, https://substackcdn.com/image/fetch/$s_!1eBd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png 848w, https://substackcdn.com/image/fetch/$s_!1eBd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png 1272w, https://substackcdn.com/image/fetch/$s_!1eBd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F016e7749-e331-41cf-84b2-9150b24ea2a3_2059x1146.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><strong>Single Endpoint Version</strong></figcaption></figure></div><p>In short, when it comes to calling constituent models to obtain intermediate results, you do have some options. However, the most common one is going to be to call those models almost exactly as you would any other preprocessing logic. Great, now back to the main example.</p><div><hr></div><h2><strong>Apply Preprocessing Logic from .py Script</strong></h2><p>Now that we have some data to work with, let's apply those preprocessing functions we defined earlier. All we need to do is specify the path to where our script lives and import our classes as we would any others off <a href="https://pypi.org/">PyPI</a>.</p><pre><code># Retrieve the current notebook's full path
notebook_path = dbutils.notebook.entry_point.getDbutils().notebook().getContext().notebookPath().get()
notebook_dir = os.path.dirname(notebook_path)
dbfs_path = '/Workspace' + notebook_dir
# This will come into play when we log the model to mlflow
custom_transformers_path = dbfs_path + '/custom_transformers.py'
from custom_transformers import JSONFlattener, EmailDomainExtractor

# # dbfs_path is already in sys.path but if it weren't we could add it like so:
# if dbfs_path not in sys.path:
#     sys.path.append(dbfs_path)</code></pre><pre><code># Split the dataset
X = df.drop(columns=['fraud', 'transaction_id'])
y = df['fraud']

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Define preprocessing steps
numeric_features = ['amount', 'account_age_days']
numeric_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())
])

categorical_features = ['transaction_type', 'customer_info.address.state', 'email_domain']
categorical_transformer = Pipeline(steps=[
    ('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
    ('onehot', OneHotEncoder(handle_unknown='ignore', sparse_output=False))
])

preprocessor = Pipeline(steps=[
    ('json_flattener', JSONFlattener(json_column='customer_info', record_prefix='customer_info')),
    ('email_domain_extractor', EmailDomainExtractor(email_column='customer_info.email')),
    ('column_transformer', ColumnTransformer(
        transformers=[
            ('num', numeric_transformer, numeric_features),
            ('cat', categorical_transformer, categorical_features)
        ])
    )
])</code></pre><p>Looking good. Remember we don't need to use an sklearn Pipeline for this to work, but we're using it anyway to keep this blog more focused on calling custom preprocessing modules on a live serving endpoint rather than building custom pipelines.</p><p>Right, let's train the model! This should take about 30 seconds on our machine with 1,000 rows, but this is far from maxing out our cluster's CPU util or RAM, so it will scale sub-linearly for orders of magnitude more data.</p><pre><code># Fit the preprocessor and transform the data
preprocessor.fit(X_train, y_train)
X_train_processed = preprocessor.transform(X_train)
X_test_processed = preprocessor.transform(X_test)

# Convert processed data to numpy arrays if they are DataFrames
if isinstance(X_train_processed, pd.DataFrame):
    X_train_processed = X_train_processed.values
if isinstance(X_test_processed, pd.DataFrame):
    X_test_processed = X_test_processed.values

# Train the XGBoost model
dtrain = xgb.DMatrix(X_train_processed, label=y_train)
dtest = xgb.DMatrix(X_test_processed, label=y_test)

params = {
    'objective': 'binary:logistic',
    'eval_metric': 'logloss',
    'seed': 42
}

bst = xgb.train(params, dtrain, num_boost_round=100)

# Evaluate the model
y_pred_proba = bst.predict(dtest)
y_pred = (y_pred_proba &gt; 0.5).astype(int)

print("Classification Report:")
print(classification_report(y_test, y_pred, digits=4))</code></pre><pre><code># Prepare artifacts and signature
joblib.dump(preprocessor, "preprocessor.joblib")
bst.save_model("model.xgb")

artifacts = {
    "preprocessor_path": "preprocessor.joblib",
    "model_path": "model.xgb"
}

sample_input = X_train.iloc[:5]
sample_output = bst.predict(xgb.DMatrix(preprocessor.transform(sample_input)))
signature = infer_signature(sample_input, sample_output)</code></pre><p>We'll now define the PyFunc wrapper model. Notice that we import our custom package again inside the <code>load_context()</code> function. This ensures all required code is saved to our model's artifacts in MLflow.</p><pre><code># Define the custom PyFunc model
class FraudDetectionModel(mlflow.pyfunc.PythonModel):
    def load_context(self, context):
        import xgboost as xgb
        import joblib
        import pandas as pd
        # Import custom transformers from the installed package
        from custom_transformers import JSONFlattener, EmailDomainExtractor

        # Load the preprocessor and model artifacts
        self.preprocessor = joblib.load(context.artifacts["preprocessor_path"])
        self.booster = xgb.Booster()
        self.booster.load_model(context.artifacts["model_path"])

    def predict(self, context, model_input):
        processed_input = self.preprocessor.transform(model_input)
        if isinstance(processed_input, pd.DataFrame):
            processed_input = processed_input.values
        dmatrix = xgb.DMatrix(processed_input)
        predictions = self.booster.predict(dmatrix)
        return predictions</code></pre><p>Additionally, we can prevent possible container build failures by specifying compatible package versions in our predefined <code>conda_env</code>:</p><pre><code>conda_env = {
    'name': 'mlflow-env',
    'channels': ['defaults'],
    'dependencies': [
        'python=3.11.0',
        'pip',
        {
            'pip': [
                'mlflow==2.17.0',
                'xgboost==2.0.3',
                'joblib==1.2.0',
                'faker==18.11.2',
                'scikit-learn==1.2.2',
                'numpy==1.23.5',
                'cloudpickle==2.2.1',
            ],
        },
    ],
}</code></pre><p>Finally we can log and register the model to MLflow. Two things I want to make note of:</p><ol><li><p>The <code>code_path</code> parameter in the <code>log_model()</code> function that we alluded to earlier is crucial for ensuring the model has access to our preprocessing code on the endpoint</p></li><li><p>We alias the latest run as <code>Production</code> and then call <code>@Production</code> at the end of the <code>model_uri</code> in the following cell; we could have omitted this and kept track of version numbers instead, but that's up to you</p></li></ol><pre><code># Log the model using mlflow.pyfunc
with mlflow.start_run() as run:
    mlflow.pyfunc.log_model(
        artifact_path="model",
        python_model=FraudDetectionModel(),
        artifacts=artifacts,
        conda_env=conda_env,
        code_paths=[custom_transformers_path],
        signature=signature,
        input_example=sample_input
    )
    model_uri = f"runs:/{run.info.run_id}/model"
    registered_model_name = "credit_card_fraud_detection_pyfunc"
    result = mlflow.register_model(
        model_uri=model_uri,
        name=registered_model_name
    )
    print(f"Model registered as {registered_model_name} with version {result.version}")

from mlflow.tracking import MlflowClient

client = MlflowClient()
client.set_registered_model_alias(
    name=registered_model_name,
    alias="Production",
    version=result.version
)</code></pre><p>We can check that this functioning as intended by loading the model from MLflow and then running some records through it:</p><pre><code># Load the model using the alias and test predictions
model_uri = f"models:/{registered_model_name}@Production"
loaded_model = mlflow.pyfunc.load_model(model_uri)

new_data = generate_data(num_rows=5)
predictions = loaded_model.predict(new_data)

print("\nPredictions:")
print(predictions)</code></pre><p>The above shows that this works in the notebook environment, but a common problem data scientists face is something working in the notebook but failing on the endpoint. We can dramatically speed up the process of testing dependency agreements and other environment variables by using the <code>mlflow.models.predict()</code> functionality too. The difference between this and <code>load_model()</code> may not seem obvious at first glance, especially because <a href="https://mlflow.org/docs/latest/python_api/mlflow.models.html#mlflow.models.predict">the documentation</a> doesn't fully explain this important distinction, but <code>mlflow.models.predict()</code> builds a lightweight virtual environment based on the <code>conda_env</code> we specified earlier, making it a much closer proxy for the endpoint than <code>load_model()</code> which uses the notebook's existing environment. You'll notice <code>mlflow.models.predict()</code> takes about 5-10x longer than <code>load_model()</code>, <strong>but a minute here may save you hours of iterative development if your alternative is waiting to see if your endpoint environment is valid!</strong></p><p>In November 2024, the <a href="https://docs.databricks.com/en/machine-learning/model-serving/model-serving-debug.html">Databricks documentation</a> for <code>mlflow.models.predict()</code> were updated to better explain this.</p><pre><code>import os
import json

# Define temporary output path
output_path = "/tmp/mlflow_predictions.json"

# This is a much better test than loaded_model.predict()
mlflow.models.predict(model_uri, new_data, output_path=output_path)

# Read predictions from the output file
with open(output_path, "r") as f:
    predictions = json.load(f)</code></pre><pre><code>mlflow.end_run()</code></pre><p>And we&#8217;re done! If you&#8217;ve made it this far, I applaud you. Let&#8217;s quickly recap what we&#8217;ve done here:</p><ul><li><p>Demonstrated how to reference an external script within a PyFunc model</p><ul><li><p>In the form of a data preprocessing pipeline for real-time inference</p></li></ul></li><li><p>Implemented an end-to-end fraud detection pipeline </p><ul><li><p>With a non-sklearn model and our custom preprocessing</p></li></ul></li><li><p>Properly packaged, logged, and registered our custom code with MLflow</p><ul><li><p>Tested both in the notebook environment and a simulated deployment environment</p></li></ul></li></ul><p>Happy coding!</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Braving through the pitfalls of LLM judges]]></title><description><![CDATA[A guide on improving your LLM evaluations.]]></description><link>https://www.databricksters.com/p/braving-through-the-pitfalls-of-llm</link><guid isPermaLink="false">https://www.databricksters.com/p/braving-through-the-pitfalls-of-llm</guid><dc:creator><![CDATA[Veena]]></dc:creator><pubDate>Tue, 15 Apr 2025 16:36:08 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!lJgz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lJgz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lJgz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg 424w, https://substackcdn.com/image/fetch/$s_!lJgz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg 848w, https://substackcdn.com/image/fetch/$s_!lJgz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!lJgz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lJgz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg" width="612" height="425" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:425,&quot;width&quot;:612,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:40529,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/161362901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lJgz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg 424w, https://substackcdn.com/image/fetch/$s_!lJgz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg 848w, https://substackcdn.com/image/fetch/$s_!lJgz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!lJgz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd67bc2de-96da-41a0-ba74-f30430f56ca0_612x425.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">a picture of Judge Judy (the original judge). </figcaption></figure></div><p>LLM judges are the de facto standard for evaluating anything related to LLMs. Human evaluations are too expensive and difficult to scale, so LLMs are a practical alternative.</p><p>But judges aren&#8217;t perfect. Here, we will examine some of the common problems with LLM judges and explore different ways to deal with them.</p><p>Please note that this blog will not cover non-LLM evaluations!</p><h2>Let us first review what Agent Evaluation looks like in Databricks.</h2><p>LLM-as-a-judge is a common evaluation technique; instead of using a human to evaluate a text response, we use an LLM. <a href="https://docs.databricks.com/aws/en/generative-ai/agent-evaluation/">Mosaic AI Agent Evaluation</a> allows you to systematically assess the quality of your agentic applications. This includes the use of LLM judges.</p><p>There are several <a href="https://docs.databricks.com/aws/en/generative-ai/agent-evaluation/llm-judge-reference">built-in judges</a> that can be used, including:</p><ul><li><p>Correctness judge: assesses whether response is accurate</p></li><li><p>Helpfulness judge: assesses if response satisfies the user</p></li><li><p>Harmlessness judge: assesses if response avoids harmful content</p></li><li><p>Coherence judge: assesses if response is logical</p></li><li><p>Relevance judge: assesses whether response addresses the query</p></li></ul><p>Given an evaluation set, you can use these judges to evaluate. Each judge takes a different set of inputs; for example, the Correctness judge requires a request, a response, and an expected response, but the Harmlessness judge only requires a request and a response. You can take a look at <a href="https://docs.databricks.com/aws/en/generative-ai/agent-evaluation/llm-judge-reference">what judges are available and how to use them</a> here.</p><h3>LLMs have a hard time with numbers.</h3><p>Let me show you what a basic implementation looks like. Using the databricks callable judge SDK, you can use the correctness judge like:</p><pre><code>from databricks.agents.evals import judges

assessment = judges.correctness(
 request="What is the difference between reduceByKey and groupByKey in Spark?",
 response="reduceByKey aggregates data before shuffling, whereas groupByKey shuffles all data, making reduceByKey more efficient.",
 expected_facts=[
   "reduceByKey aggregates data before shuffling",
   "groupByKey shuffles all data",
 ]
)</code></pre><p>We can see that an assessment contains information something like this:</p><pre><code>Assessment: 
error_code=None
error_message=None
metadata={}
name='correctness'
rationale="..." 
value=CategoricalRating.YES</code></pre><p>The value that the assessment returned is categorical. LLMs notably struggle quite a lot with numerical scoring. Some studies show that they have preferences for certain values. Other studies often show them clustering around the highest and lowest values, instead of utilizing the full range.</p><p>Let us assume you have already created a judge to output scores from 1 to 10. When graphing the scores with the &#8220;ideal&#8221; scores, you could see something like this: </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Jwni!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Jwni!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png 424w, https://substackcdn.com/image/fetch/$s_!Jwni!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png 848w, https://substackcdn.com/image/fetch/$s_!Jwni!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png 1272w, https://substackcdn.com/image/fetch/$s_!Jwni!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Jwni!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png" width="800" height="470" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d78a8622-469a-4803-b210-91df22e37101_800x470.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:470,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:59982,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/161362901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb1a1230d-b74d-413e-8724-833b3f805619_800x500.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Jwni!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png 424w, https://substackcdn.com/image/fetch/$s_!Jwni!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png 848w, https://substackcdn.com/image/fetch/$s_!Jwni!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png 1272w, https://substackcdn.com/image/fetch/$s_!Jwni!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd78a8622-469a-4803-b210-91df22e37101_800x470.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">In this example, the LLM gives perfect scores until a certain threshold, where it drops to very low scores. Note: this graph was created manually via matplotlib. </figcaption></figure></div><p>The built-in judge already includes a categorical value instead of a numerical one, but if you need numerical ratings, <a href="https://arxiv.org/abs/2310.08491">prompt the judge with an explanation for each of the scores.</a> In MLFlow, you can include evaluation examples when defining different evaluation metrics.</p><pre><code>average_example = EvaluationExample(
    input="What are the main types of horse breeds?",
    output="The main horse breeds include Arabian, Thoroughbred, Quarter Horse, Appaloosa, Morgan, Tennessee Walker, Clydesdale, and Mustang. Arabians are known for endurance, Thoroughbreds for racing, Quarter Horses for sprinting, and Clydesdales for their size and strength.",
    score=5,
    justification="This response correctly lists 8 common horse breeds and provides brief descriptions for 4 of them, but the descriptions are very basic and only cover half of the breeds mentioned. It lacks depth about breed characteristics, historical origins, or typical uses.",
    grading_context={
        "targets": "There are numerous horse breeds worldwide, with common breeds including Arabian, Thoroughbred, Quarter Horse, Appaloosa, Morgan, Tennessee Walker, Andalusian, Friesian, Clydesdale, Percheron, Mustang, and Shetland Pony. Each breed has distinctive physical traits, temperaments, and was developed for specific purposes like racing, work, or riding."
    },
)

horse_breed_similarity_metric = answer_similarity(
    examples=[poor_example, average_example, excellent_example])</code></pre><h3>LLMs like long answers.</h3><p>In my last example regarding horses, you can see that the average example was scored lower than it would have been because it lacked &#8216;depth.&#8217; Unfortunately, lots of LLMs equate depth with a lot of unnecessary chatter. When graphing scores of responses of equal quality, you may see long responses rewarded more than short responses, like: </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!WoeS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!WoeS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png 424w, https://substackcdn.com/image/fetch/$s_!WoeS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png 848w, https://substackcdn.com/image/fetch/$s_!WoeS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png 1272w, https://substackcdn.com/image/fetch/$s_!WoeS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!WoeS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png" width="800" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:39998,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/161362901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!WoeS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png 424w, https://substackcdn.com/image/fetch/$s_!WoeS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png 848w, https://substackcdn.com/image/fetch/$s_!WoeS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png 1272w, https://substackcdn.com/image/fetch/$s_!WoeS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F661bd0b0-f290-45b2-a50e-44ab162298e3_800x500.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">In this example, the LLM judge scores rise with response length, even when the actual quality of the content remains constant. Note: this graph was generated manually via matplotlib. </figcaption></figure></div><p><a href="https://arxiv.org/abs/2310.10076">LLM judges tend to prefer longer outputs</a>. This makes sense if you have ever used one of these chat bots. Depending on your use case, this might not be preferable. When I am talking to a customer service chatbot, for example, I get frustrated when it responds with paragraph long responses to my simple questions. Conciseness is incredibly important. This problem could also mean that more accurate responses are drowned out by rambling, semi-accurate responses. Observe what is getting approved by your judge to make sure the LLM is not avoiding brevity.</p><p>If you are noticing that only long answers are getting approved, you can instead simply adjust scores based on the length, penalizing answers that are &#8216;too&#8217; long.</p><p>Let us assume that you have extracted the base score from the judge. You can have a simple function that penalizes a longer answer, if it surpasses a hardcoded threshold.</p><pre><code>length_ratio = response_length / max(1, request_length)

def linear_verbosity_adjustment(length_ratio, base_score):
    threshold = 3.0
    if length_ratio &lt;= threshold:
        return 0
    else:
        return min(base_score * 0.3, (length_ratio - threshold) * 0.5)</code></pre><p>But you can also approach this in a more sophisticated manner. <a href="https://arxiv.org/pdf/2404.04475">In this paper</a>, they fit a regression model to predict &#8220;what would the score be if the responses all had the same length?&#8221; This improved correlation with human preferences from 0.94 to 0.98, but in most cases, however, this is overkill.</p><h3>LLMs are biased towards themselves.</h3><p>We have also seen that<a href="https://arxiv.org/html/2405.01724v1"> LLM judges have a preference for text with lower perplexity</a>. This suggests that LLMs prefer language similar to language they were trained on. This can lead to your evaluators assigning higher scores to outputs generated by their own kind. For example, you can no longer trust a GPT model to evaluate a Llama 8B model against a GPT 4o-mini model without bias.</p><p>You can mitigate this bias by using a jury-- a collection of LLM judges instead. The goal here is to use LLMs from different families, so one LLM&#8217;s bias towards the answer does not prevent you from understanding the quality of the response. You can create a custom metric in Databricks to do this. Here, I am defining three different judges using different LLMs with the same prompt.</p><pre><code>import mlflow
from mlflow.metrics.genai import make_genai_metric_from_prompt
from databricks.agents.evals import metric
from databricks.agents.evals import judges
from mlflow.evaluation import Assessment


judge_prompt = """
Determine if this response accurately covers all expected facts.

Request: '{inputs}'
Response: '{response}'
"""

llama_judge = make_genai_metric_from_prompt(
 name="accuracy_judge1",
 judge_prompt=judge_prompt,
 model="endpoints:/databricks-meta-llama-3-1-405b-instruct",
 metric_metadata={"assessment_type": "ANSWER"}
)

claude_judge = make_genai_metric_from_prompt(
 name="accuracy_judge2",
 judge_prompt=judge_prompt,
 model="endpoints:/databricks-claude-3-7-sonnet",
 metric_metadata={"assessment_type": "ANSWER"},
 )

gpt_judge = make_genai_metric_from_prompt(
 name="accuracy_judge3",
 judge_prompt=judge_prompt,
 model="endpoints:/test-gpt-endpoint",
 metric_metadata={"assessment_type": "ANSWER"},
 )</code></pre><p>Then, take all of these individual judges and define a custom metric. I am averaging the scores outputted from each judge here, but if you want to avoid numeric values, you can instead average across boolean values. If you are seeing that the metric is too &#8216;easy&#8217; to pass, you can set a numeric threshold that must be passed by each judge. This would make sense for higher risk use cases.</p><pre><code>@metric
def llm_jury(request, response):
   inputs = request['messages'][0]['content']

   llama_metric_result = llama_judge(inputs=inputs, response=response)
   claude_metric_result = claude_judge(inputs=inputs, response=response)
   gpt_metric_result = gpt_judge(inputs=inputs, response=response)


   int_score = llama_metric_result.scores[0] + claude_metric_result.scores[0] + gpt_metric_result.scores[0]
   int_score = int_score/3

   return [
       Assessment(
           name="llm_jury_score",
           value=int_score,
           rationale=f"LLAMA: {llama_metric_result.scores[0]:.2f}, Claude: {claude_metric_result.scores[0]:.2f}, GPT: {gpt_metric_result.scores[0]:.2f}"
       )
   ]</code></pre><h3>LLMs produce inconsistent evals.</h3><p>Judges can output significantly different results if you prompt it multiple times. By default, using <code>make_genai_metric_from_prompt</code> uses <code>temperature=0.0</code> and <code>top_p=1.0</code>, but there is still a chance that the LLM judges output different results (<a href="https://www.databricksters.com/p/on-the-topic-of-llms-and-non-determinism">if you have time, read Austin&#8217;s blog on LLM non-determinism here</a>).</p><p>You can test consistency with a judge by prompting it multiple times. In a perfect world, the same prompt would get the same scores, but other times, you may get something like this: </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LUM8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LUM8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png 424w, https://substackcdn.com/image/fetch/$s_!LUM8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png 848w, https://substackcdn.com/image/fetch/$s_!LUM8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png 1272w, https://substackcdn.com/image/fetch/$s_!LUM8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LUM8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png" width="800" height="500" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:500,&quot;width&quot;:800,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:64840,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/161362901?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LUM8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png 424w, https://substackcdn.com/image/fetch/$s_!LUM8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png 848w, https://substackcdn.com/image/fetch/$s_!LUM8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png 1272w, https://substackcdn.com/image/fetch/$s_!LUM8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0246cfe1-175d-4548-a95a-2a42e6360f9d_800x500.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">In this example, we can see that the same prompt gets different scores. Note: this is a graph manually made in matplotlib. </figcaption></figure></div><p>There are a few ways to address this. You can treat this like how we treated familiarity bias. Instead of prompting an LLM once, prompt it multiple times (5 times) and keep the majority output. But because this option can increase cost a lot, you can try to more intelligently prompt the judge itself. Try something like chain-of-thought reasoning, and <a href="https://arxiv.org/abs/2310.08491">ask the model for its reasoning before outputting a binary score.</a> This can force a more deliberate consideration and reduce random variation.</p><h3>We are an intermediate stage of LLM evaluation.</h3><p>Current LLM judges are useful but imperfect tools. So, where does that leave us? </p><p>Start by identifying which biases most significantly impact your specific use case. For customer support bots, length bias might be your primary concern. For factual assessment, inconsistency may be more problematic. Apply targeted solutions for the problems you are immediately seeing, instead of trying to solve every issue at once. </p><p>We can expect evaluation techniques to continue to improve. Remember the goal is not to implement every possible mitigation, but to build a system that provides consistent and actionable feedback.  </p><p>I hope this guide was helpful. Please comment if you have any questions. </p><p>Happy judging!</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Databricks Vector Search Similarity Scores Deep Dive ]]></title><description><![CDATA[Have you noticed unexpected results from Databricks Vector Search&#8217;s similarity_search?]]></description><link>https://www.databricksters.com/p/databricks-vector-search-similarity</link><guid isPermaLink="false">https://www.databricksters.com/p/databricks-vector-search-similarity</guid><dc:creator><![CDATA[Joshua Eason]]></dc:creator><pubDate>Wed, 09 Apr 2025 18:57:37 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!EoBW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you noticed unexpected results from Databricks Vector Search&#8217;s similarity_search? If you&#8217;re coming from a cosine similarity background, the scores might seem puzzlingly misaligned with your expectations. In this blog, we&#8217;ll dive deep into why this happens, explain the key differences between similarity metrics, and provide a solution to bridge this gap.</p><p><strong>Cosine Similarity</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p>Let&#8217;s start with a quick refresher on cosine similarity. The cosine similarity between two vectors a and b defined as:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{cosine_sim}(a, b) = \\frac{\\vec{a} \\cdot \\vec{b}}{\\|\\vec{a}\\| \\cdot \\|\\vec{b}\\|}\n&quot;,&quot;id&quot;:&quot;PTHFFWHRWL&quot;}" data-component-name="LatexBlockToDOM"></div><p></p><p><strong>Geometric Interpretation</strong></p><p>Cosine similarity gives us a clear geometric intuition for vector similarity by measuring the angle between them in vector space. If two vectors point in the same direction, their cosine similarity is 1; if they are orthogonal (at 90&#176;), it is 0; and if they point in opposite directions, it is &#8211;1. This works because cosine similarity is simply the cosine of the angle between them in their vector space.</p><p>Think of it like comparing the orientation of arrows: two arrows pointing in the same direction, no matter how long, are perfectly aligned (cosine = 1), while arrows at right angles share no alignment (cosine = 0), and arrows pointing in opposite directions are fully misaligned (cosine = &#8211;1). Unlike Euclidean distance, which can be influenced by the length of the vectors, cosine similarity purely reflects alignment, making it especially useful in high-dimensional spaces where magnitude may vary but direction carries semantic meaning.</p><p>Here is a sample implementation</p><pre><code>import numpy as np

def cosine_similarity(a, b):
    a = np.array(a)
    b = np.array(b)
    dot_product = np.dot(a, b)
    norm_a = np.linalg.norm(a)
    norm_b = np.linalg.norm(b)
    return dot_product / (norm_a * norm_b)</code></pre><p>In practice, many of us rely on the scikit-learn implementation.</p><pre><code>import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

# Example vectors
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])

# Reshape for sklearn (expects 2D arrays)
similarity = cosine_similarity(a.reshape(1, -1), b.reshape(1, -1))[0][0]</code></pre><p>Interestingly, when the input vectors are normalized to unit length so that </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\|\\vec{a}\\| = \\|\\vec{b}\\| = 1&quot;,&quot;id&quot;:&quot;ESCYZSPSAJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>cosine similarity reduces to a simple dot product:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{cosine_sim}(a, b) = \\vec{a} \\cdot \\vec{b}&quot;,&quot;id&quot;:&quot;DXZOPUTNMJ&quot;}" data-component-name="LatexBlockToDOM"></div><p></p><p>In cases where we want to set a bound, cosine_sim &#8712;<em> </em>[0, 1], we may apply a linear transformation such as </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;bounded\\_cosine = \\frac{cosine\\_sim +1}{2}&quot;,&quot;id&quot;:&quot;UEFISHOVCQ&quot;}" data-component-name="LatexBlockToDOM"></div><p></p><p>which preserves order, and simply squashes the outputs of your cosine similarity function into the new range. This can be very beneficial in circumstances where your similarity scores are used as intermediate inputs to downstream models that have strict boundary requirements, or are sensitive to negative values.</p><p>Another variation that you may have seen before is the cosine distance, which is just 1 <em>&#8722; </em>cosine_sim. This is often used in conjunction with the other transformations, but provides an interpretation based on the distance (smaller is closer) instead of the alignment (higher is closer).</p><p><strong>Databricks&#8217; Similarity Computation</strong></p><p>It is possible that you have noticed that neither of these methods produce the scores that you see when you perform a similarity_search using the Databricks VectorSearchIndex class. In contrast, from the <a href="https://docs.databricks.com/aws/en/generative-ai/vector-search%23keyword-search-algorithm">Official Databricks</a> <a href="https://docs.databricks.com/aws/en/generative-ai/vector-search%23keyword-search-algorithm">Documentation,</a> we can see that databricks uses the following formula to compute similarity:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{similarity} = \\frac{1}{(1 + \\text{dist(q, x)}^2)}&quot;,&quot;id&quot;:&quot;DFWBHUQRVM&quot;}" data-component-name="LatexBlockToDOM"></div><p></p><p>Where</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{dist}(q, x) = \\sqrt{(q_1 - x_1)^2 + (q_2 - x_2)^2 + \\cdots + (q_d - x_d)^2}&quot;,&quot;id&quot;:&quot;KNJMSRDIAL&quot;}" data-component-name="LatexBlockToDOM"></div><p></p><p>While this formula relies on the euclidean distance, you can see that it is not just the euclidean distance. In contrast to cosine similarity, which compares <strong>direction</strong>, this score relies on a linear transformation of Euclidean distance, which compares <strong>position</strong>. If the vectors are not normalized, the algorithm sensitive to both the <strong>magnitude </strong>and <strong>alignment </strong>of the vectors. For example:</p><ul><li><p>Two vectors pointing in the same direction but with very different lengths may have high cosine similarity but large Euclidean distance.</p></li><li><p>Conversely, two vectors that are numerically close (in terms of component values) but not aligned may have small Euclidean distance but low cosine similarity.</p></li></ul><p>This positional sensitivity makes the score potentially misleading in semantic spaces (like embeddings) where <strong>direction </strong>is more meaningful than length.</p><p>For clarity, here&#8217;s the Databricks similarity scoring function referenced in our examples:</p><pre><code>def euclidean_distance(q, x):
    """Calculate Euclidean distance between vectors q and x"""
    q = np.array(q)
    x = np.array(x)
    return np.linalg.norm(q - x)

def databricks_similarity_score(q, x):
    """similarity score based on the formula: 1 / (1 + dist(q, x)^2)"""
    distance = euclidean_distance(q, x)
    return 1 / (1 + distance ** 2)</code></pre><p><strong>Side Note - Hybrid Similarity Score</strong></p><p>There is an additional step for providing the score if a hybrid search, if using the <code>query_type='HYBRID'</code> argument when calling the <code>VectorSearchIndex.similarity_search</code> method, a composite BM25 Keyword and Vector similarity is used. This score</p><p>relies on an algorithm called Reciprocal Rank Fusion (RRF), and aggregates rankings from several sources into a single, ranking. For more detailed information about this, please see <a href="https://plg.uwaterloo.ca/~gvcormac/cormacksigir09-rrf.pdf">Ref (1).</a></p><p><strong>Vector Normalization</strong></p><p>Vector normalization rescales a vector to have unit length (i.e., L2 norm of 1). This process preserves the vector&#8217;s direction while standardizing its magnitude, which is crucial for reliable similarity comparisons.</p><p>Given a vector a, its normalized form is:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{a} = \\frac{\\vec{a}}{\\|\\vec{a}\\|}&quot;,&quot;id&quot;:&quot;VQMRGEQGPD&quot;}" data-component-name="LatexBlockToDOM"></div><p>Normalization essentially projects all vectors onto the unit hypersphere in n-dimensional space, allowing us to compare them purely by their direction and eliminating magnitude differences that can distort similarity measures.</p><p>Here is the vanilla python implementation</p><pre><code>import numpy as np

def l2_normalize(vector):
    vector = np.array(vector)
    norm = np.linalg.norm(vector)
    if norm == 0:
        return vector  # Avoid division by zero
    return vector / norm</code></pre><p>Though in practice, most of us rely on the scikit learn implementation</p><pre><code>from sklearn.preprocessing import normalize
import numpy as np

# Each row will be treated as a separate vector
X = np.array([[1, 2, 3], [4, 5, 6]])

# Normalize along rows (axis=1)
X_normalized = normalize(X, norm='l2', axis=1)</code></pre><p><strong>Why Normalize?</strong></p><p>So, why is normalization so important?</p><p>This sketch illustrates the geometric difference between cosine similarity and the Databricks similarity score, which is derived from squared Euclidean distance.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!EoBW!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!EoBW!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png 424w, https://substackcdn.com/image/fetch/$s_!EoBW!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png 848w, https://substackcdn.com/image/fetch/$s_!EoBW!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png 1272w, https://substackcdn.com/image/fetch/$s_!EoBW!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!EoBW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png" width="1456" height="938" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:938,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:105904,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/160948890?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!EoBW!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png 424w, https://substackcdn.com/image/fetch/$s_!EoBW!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png 848w, https://substackcdn.com/image/fetch/$s_!EoBW!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png 1272w, https://substackcdn.com/image/fetch/$s_!EoBW!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F21c41254-75f7-4e6a-a85b-72bd78781e03_1489x959.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p></p><p>The green angles between vectors (e.g., cos(a, b) and cos(b, c)) represent cosine similarity, which depends purely on direction. In contrast, the orange segments represent Euclidean distances between vector tips &#8212; and since the Databricks score is computed as a linear transformation of the distance, these distances directly influence the similarity score.</p><p>When embeddings are not normalized, the magnitude of the vectors affects the result. Even though Vector b (raw) is directionally aligned with Vector a, its large magnitude causes the straight-line distance dist(a, b_raw) to be quite large &#8212; leading to a low Databricks score. At the same time, Vector c, which is closer in space but less aligned in direction, will have a smaller Euclidean distance to a, and therefore a higher similarity score under the Databricks formula. This misrepresents their true semantic similarity if you were expecting cosine-like behavior.</p><p>This is why normalization is essential: when all vectors are normalized (as with Vector b (norm&#8217;d)), the Databricks similarity score becomes a function of angle alone &#8212; effectively mirroring cosine similarity.</p><p>While normalization offers a range of benefits &#8212; like simplifying cosine similarity computation, removing scale bias, and improving behavior in clustering &#8212; the key reason we normalize <strong>embedding vectors in Databricks Vector Search </strong>is to ensure <strong>rank-order equivalence </strong>to cosine similarity.</p><p><strong>Rank-Order Equivalence</strong></p><p><strong>Rank-Order Equivalence</strong>: When vectors are normalized to unit length, the ranking of results by L2 distance matches the ranking by cosine similarity, even though the actual numerical score values differ.</p><p>When all vectors are normalized to unit length, the rank order of L2 distances becomes <strong>equal to </strong>the rank order of cosine similarities. This allows you to take advantage of fast approximate L2 search while still reasoning about results as if you&#8217;re using cosine similarity.</p><p>One misconception is that this relationship is numerically equal (similarity scores will be exactly equal between the two). This is not the case, and they can be very different in some cases. However, if you consider only their order in the ranking, that relationship is preserved, assuming embeddings are normalized.</p><p><strong>Model-Specific Note: GTE vs. BGE</strong></p><ul><li><p>The <strong>BGE </strong>(BAAI General Embedding) model <strong>produces normalized embeddings </strong>by default.</p></li><li><p>The <strong>GTE </strong>(General Text Embedding) model <strong>does not </strong>produce normalized embeddings out of the box.</p></li><li><p>Other <strong>External </strong>models may or may not provide normalized embeddings, so ensure that you check this if using external models.</p></li></ul><p>If you&#8217;re using embeddings from a model that are not normalized by default in Databricks Vector Search, it is <strong>strongly recommended to normalize them before indexing</strong>. This can be done with any L2 normalization function (like sklearn.preprocessing.normalize). Additionally, you must apply the <strong>same normalization strategy to your query vectors at retrieval time </strong>that you used when building the index. In other words, if you normalize during indexing, you must also normalize your queries, and if you don&#8217;t, then skip it at query time too.</p><p>Failing to match the normalization behavior between index and query time will result in invalid similarity comparisons and misleading results. Consistency is key to ensuring your search results are meaningful and accurate.</p><p>As demonstrated in these examples, when all vectors are properly normalized, the ranking order is identical between cosine similarity and Databricks similarity scores. This confirms the rank-order equivalence principle, which is crucial for reliable vector search.</p><p><strong>Examples</strong></p><p>Consider the following code:</p><pre><code>import numpy as np
from sklearn.metrics.pairwise import cosine_similarity, euclidean_distances
from sklearn.preprocessing import normalize

# Example vectors
a = np.array([1.0, 0.0])
b = np.array([0.0, 1.0])
c = np.array([1.0, 1.0])
d = np.array([0.2, 0.8])

cos_distance = cosine_similarity(a.reshape(1, -1), b.reshape(1, -1))[0][0]
db_distance = get_databricks_similarity_score(a, b)

print(f"cosine(a,b): {cos_distance}")
print(f"db_distance(a,b): {db_distance}")

vectors = {
    "a": a,
    "b": b,
    "c": c,
}

cosine_scores = {}
db_scores = {}

for name, vec in vectors.items():
    cosine_scores[name] = cosine_similarity(d.reshape(1, -1), vec.reshape(1, -1))[0][0]
    db_scores[name] = get_databricks_similarity_score(d, vec)

cosine_ranking = sorted(cosine_scores.items(), key=lambda x: x[1], reverse=True)
db_ranking = sorted(db_scores.items(), key=lambda x: x[1], reverse=True)

print("\n--- Pairwise Similarity to 'd' ---")
print("Cosine Similarity Scores:")
for name, score in cosine_ranking:
    print(f"{name}: {score:.4f}")

print("\nDatabricks (L2-based) Similarity Scores:")
for name, score in db_ranking:
    print(f"{name}: {score:.4f}")

print("\nCosine Similarity Ranking:", [name for name, _ in cosine_ranking])
print("Databricks Similarity Ranking:", [name for name, _ in db_ranking])</code></pre><p>Which will produce the following outputs</p><pre><code>cosine(a,b): 0.0
db_distance(a,b): 0.33333333333333326

--- Pairwise Similarity to 'd' ---
Cosine Similarity Scores:
b: 0.9701
c: 0.8575
a: 0.2425

Databricks (L2-based) Similarity Scores:
b: 0.9259
c: 0.5952
a: 0.4386

Cosine Similarity Ranking: ['b', 'c', 'a']
Databricks Similarity Ranking: ['b', 'c', 'a']</code></pre><p>Notice that while the numerical scores are much different, the rankings are</p><p>preserved. This is because the input vectors in the example are all normalized by definition.</p><p>Lets try that again, but this time, we will use arbitrary vectors that are not normalized.</p><pre><code># Define non-normalized versions of the same vectors
a_raw = np.array([2.0, 0.0])
b_raw = np.array([0.0, 3.0])
c_raw = np.array([5.0, 5.0])
d_raw = np.array([1.0, 4.0])</code></pre><p>Below, we can see that the results no longer respect rank-order equivalence</p><pre><code>--- Pairwise Similarity to 'd_raw' (Non-Normalized Vectors) ---
Cosine Similarity Scores (Non-Normalized):
b_raw: 0.9701
c_raw: 0.8575
a_raw: 0.2425

Databricks (L2-based) Similarity Scores (Non-Normalized):
b_raw: 0.3333
a_raw: 0.0556
c_raw: 0.0556

Cosine Similarity Ranking (Non-Normalized): ['b_raw', 'c_raw', 'a_raw']
Databricks Similarity Ranking (Non-Normalized): ['b_raw', 'a_raw', 'c_raw']</code></pre><p>Notice how the rankings diverged when using non-normalized vectors! Vector c_raw dropped from second to last place in the Databricks ranking despite main- taining its cosine position. This clearly demonstrates why proper normalization is essential when working with Databricks Vector Search if you want results that align with semantic expectations.</p><p>And, if we normalize the raw vectors above</p><pre><code>a_norm = normalize(a_raw.reshape(1,-1))
b_norm = normalize(b_raw.reshape(1,-1))
c_norm = normalize(c_raw.reshape(1,-1))
d_norm = normalize(d_raw.reshape(1,-1))</code></pre><p>and recheck them, we can see that the relationship is again preserved</p><pre><code>--- Pairwise Similarity to 'd_norm' (Normalized Vectors) ---
Cosine Similarity Scores (After Normalization):
b_norm: 0.9701
c_norm: 0.8575
a_norm: 0.2425

Databricks (L2-based) Similarity Scores (After Normalization):
b_norm: 0.9436
c_norm: 0.7782
a_norm: 0.3976

Cosine Similarity Ranking (Normalized): ['b_norm', 'c_norm', 'a_norm']
Databricks Similarity Ranking (Normalized): ['b_norm', 'c_norm', 'a_norm']</code></pre><p><strong>A Bridge Between These Metrics???</strong></p><p>Suppose that you require the interpretability of actual cosine similarity scores. For example, you want to apply semantic thresholds, visualize search relevance, or feed scores into a downstream model. But you still want to take advantage of the fast, scalable L2-based indexing provided by Databricks Vector Search.</p><p>What do you do?</p><p><strong>The Good News</strong></p><p>If your embedding vectors are <strong>L2-normalized </strong>before indexing, and your query vectors are also normalized at retrieval time, there&#8217;s a direct mathematical relationship between the <strong>Databricks similarity score </strong>and <strong>cosine similarity</strong>. That means you can <strong>recover cosine similarity </strong>&#8212; exactly &#8212; from the score that Databricks returns.</p><p><strong>The Algebra</strong></p><p>The following derivation shows how to convert between Databricks similarity scores and cosine similarity. If you&#8217;re primarily interested in the practical application, feel free to skip to the &#8220;Final Formula&#8221; section.</p><p>Let&#8217;s recall how Databricks computes similarity:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{score} = \\frac{1}{1 + \\|\\vec{a} - \\vec{b}\\|^2}&quot;,&quot;id&quot;:&quot;PESACYAPBN&quot;}" data-component-name="LatexBlockToDOM"></div><p>If both vectors, a and b are <strong>L2-normalized</strong>, then:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\|\\vec{a} - \\vec{b}\\|^2 = 2(1 - \\cos(\\theta))&quot;,&quot;id&quot;:&quot;GVAQDVEMUB&quot;}" data-component-name="LatexBlockToDOM"></div><p>Plug that into the Databricks score:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{score} = \\frac{1}{1 + 2(1 - \\cos(\\theta))} = \\frac{1}{3 - 2\\cos(\\theta)}&quot;,&quot;id&quot;:&quot;XTHGZXTCNG&quot;}" data-component-name="LatexBlockToDOM"></div><p>Now solve for cos(&#952;) from the Databricks score:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{score} = \\frac{1}{3 - 2\\cos(\\theta)}&quot;,&quot;id&quot;:&quot;AAXNMUIUAR&quot;}" data-component-name="LatexBlockToDOM"></div><p>Invert both sides:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;3 - 2\\cos(\\theta) = \\frac{1}{\\text{score}}&quot;,&quot;id&quot;:&quot;DMKQDQMSKI&quot;}" data-component-name="LatexBlockToDOM"></div><p>Move terms:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;-2\\cos(\\theta) = \\frac{1}{\\text{score}} - 3&quot;,&quot;id&quot;:&quot;HMXWBFFFQT&quot;}" data-component-name="LatexBlockToDOM"></div><p>Finally, divide both sides and simplify:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\cos(\\theta) = 1 - \\frac{1}{2} \\left( \\frac{1}{\\text{score}} - 1 \\right)&quot;,&quot;id&quot;:&quot;JSTDCLNOPZ&quot;}" data-component-name="LatexBlockToDOM"></div><p></p><p><strong>Final Formula</strong></p><p>Assuming normalized vectors:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\cos(\\theta) = 1 - \\frac{1}{2} \\left( \\frac{1}{\\text{score}} - 1 \\right)&quot;,&quot;id&quot;:&quot;JZGQHHJYVC&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>Sample Implementation</strong></p><p>Lets implement this in vanilla python so we can test it out!</p><pre><code>def cosine_from_databricks_score(score):
    """Convert Databricks similarity to cosine similarity"""
    return 1 - 0.5 * (1 / score - 1)</code></pre><p><strong>Experimentation</strong></p><pre><code>from sklearn.metrics.pairwise import cosine_similarity
from sklearn.preprocessing import normalize

# Define example vectors
a = np.array([1.0, 2.0, 3.0])
b = np.array([4.0, 5.0, 6.0])

# Normalize the vectors
a_norm = normalize(a.reshape(1, -1))[0]
b_norm = normalize(b.reshape(1, -1))[0]

# Compute cosine similarity directly
cosine_true = cosine_similarity(a_norm.reshape(1, -1), b_norm.reshape(1, -1))[0][0]

# Compute Databricks-style similarity score implemented eariler
db_score = databricks_similarity_score(a_norm, b_norm)

# Convert back to cosine similarity
cosine_reconstructed = cosine_from_databricks_score(db_score)

print(f"True cosine similarity:        {cosine_true:.8f}")
print(f"Databricks score:              {db_score:.8f}")
print(f"Reconstructed cosine from DB:  {cosine_reconstructed:.8f}")</code></pre><p>Which produces the following output</p><pre><code>True cosine similarity:        0.97463185
Databricks score:              0.95171357
Reconstructed cosine from DB:  0.97463185</code></pre><p><strong>Why This Is Precise</strong></p><p>This formula is <strong>algebraically exact </strong>&#8212; not an approximation &#8212; under the key assumption that both vectors are normalized. In that case, the relationship between cosine similarity and L2 distance becomes a clean geometric identity, and the Databricks score becomes a transformed version of cosine similarity.</p><p>The only sources of deviation would be:</p><ul><li><p><strong>Numerical precision issues </strong>(e.g., in high dimensions)</p></li><li><p><strong>Vectors not being normalized</strong></p></li></ul><p>As long as normalization is handled properly, this conversion is <strong>100% faithful</strong></p><p>to what cosine similarity would return.</p><p><strong>Why You Might Want to Use It</strong></p><ul><li><p><strong>Recover interpretability</strong>: You get the familiar cosine scale of &#8211;1 to 1, or optionally [0, 1] if re-bounded</p></li><li><p><strong>Apply thresholds</strong>: Use well-understood semantic cutoffs like 0.75 or 0.9</p></li><li><p><strong>Post-process top-</strong><em><strong>k </strong></em><strong>results</strong>: After retrieving candidates from the index, re-score using this formula and sort/filter as needed</p></li><li><p><strong>Blend with other cosine-based systems</strong>: Helps when migrating to or integrating with other platforms that rely on cosine similarity</p></li></ul><p>In short: this gives you the best of both worlds &#8212; the performance of L2-based search with the intuitive power of cosine similarity.</p><p><strong>Conclusion</strong></p><p>So there you have it! The mystery of Databricks Vector Search similarity scoring demystified. Let&#8217;s recap what we&#8217;ve learned on this mathematical journey:</p><ol><li><p><strong>Cosine similarity </strong>focuses on the alignment (direction) of vectors, making it ideal for semantic similarity in embedding spaces.</p></li><li><p><strong>Databricks similarity </strong>measures positional differences stemming from reliance on L2, straight line, Euclidean distance.</p></li><li><p><strong>Normalization </strong>is the critical bridge between these two worlds &#8212; when vectors are normalized, the rank order of results becomes equivalent between cosine similarity and Databricks&#8217; scoring system.</p></li><li><p>If you need actual cosine similarity values (not just rankings), you can precisely recover them from Databricks scores using our conversion formula</p></li></ol><p>This understanding gives you the best of both worlds: the performance and scale of Databricks Vector Search with the interpretability and familiarity of cosine similarity. No more scratching your head when similarity scores don&#8217;t match!</p><p>Remember that consistency is key &#8212; if you normalize your vectors during indexing (which you absolutely should for most embedding models), make sure to apply the same normalization to your query vectors at search time. And if you&#8217;re working with models like GTE that don&#8217;t normalize by default, take that extra step to ensure your embeddings live on the unit sphere.</p><p>Armed with these insights, you can now confidently build sophisticated vector search applications in Databricks that behave exactly as you expect them to. Happy searching!</p><p>Sources: (1) Cormack, G. V., Clarke, C. L., &amp; Buettcher, S. (2009, July). Reciprocal rank fusion outperforms condorcet and individual rank learning methods. In Proceedings of the 32nd international ACM SIGIR conference on Research and development in information retrieval (pp. 758-759).</p><p> </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[On the Topic of LLMs and Non-Determinism:]]></title><description><![CDATA[Practical Limitations in Combating the Myth of Uncertainty in Deep Learning]]></description><link>https://www.databricksters.com/p/on-the-topic-of-llms-and-non-determinism</link><guid isPermaLink="false">https://www.databricksters.com/p/on-the-topic-of-llms-and-non-determinism</guid><dc:creator><![CDATA[Austin]]></dc:creator><pubDate>Tue, 18 Mar 2025 14:02:55 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!c2pQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Have you ever been told that Deep Learning algorithms are inherently non-deterministic? Or that GPUs, PyTorch, TensorFlow, or LLMs are? Until recently, I also believed it to be an unfortunate fact of life that due to both hardware and software limitations I didn't fully understand, there was no way to get deterministic output from a deep learning model. But what are those specific limitations? Over the next few minutes I want to explore the most frequently cited sources of non-determinism in deep learning systems, why they exist, and where they can be overcome.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!c2pQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!c2pQ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg 424w, https://substackcdn.com/image/fetch/$s_!c2pQ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg 848w, https://substackcdn.com/image/fetch/$s_!c2pQ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!c2pQ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!c2pQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg" width="300" height="300" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:300,&quot;width&quot;:300,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:11743,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.databricksters.com/i/159304619?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!c2pQ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg 424w, https://substackcdn.com/image/fetch/$s_!c2pQ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg 848w, https://substackcdn.com/image/fetch/$s_!c2pQ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!c2pQ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe6b06023-4434-4a73-b62d-7209a3f1f90b_300x300.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Democritus: An early determinist philosopher and earlier day drinker</figcaption></figure></div><p>Let's begin with the low-level hardware operations. A major hurdle to deep learning determinism is the non associative properties of floating point arithmetic. Anyone with a CS background has probably seen something like this before:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><pre><code><code>## This prints False btw
print((0.7 + 0.2 + 0.1) == 1)</code></code></pre><p>Some of you are already reciting the reason in your mind: <a href="https://en.m.wikipedia.org/wiki/Dyadic_rational">dyadic rationals</a>. In simple terms, if a number can be expressed as a fraction whose denominator is a power of two, then it can be expressed exactly in finite binary representation.</p><p>If addition can't be considered deterministic, then what hope do we possibly have in making the billions of matrix operations that must take place to predict token sequences deterministic? Matrix multiplication does not suffer from the non-associative property precisely because it follows a fixed sequence of operations for which the computation pattern is consistent across most hardware. Dot products are computed in a defined order. Consider the below chunk of code from the <a href="https://www.twosigma.com/articles/a-workaround-for-non-determinism-in-tensorflow/">excellent Two Sigma blog</a> on this same topic, that showcases the comparative non-determinism of a naively implemented <code>tf.reduce_sum()</code> operation in TensorFlow versus one that leverages <code>tf.matmul()</code>:</p><pre><code>## Only runnable in TensorFlow 1.x
import tensorflow as tf
import numpy as np
N = 100
S = (1, 100000)
np.random.seed(1)
r = np.random.normal(0, 100, S).astype(np.float32)
x = tf.placeholder(tf.float32, S)
examples = {
    'reduce_sum': tf.reduce_sum(x),
    'reduce_sum_det': tf.matmul(x, tf.ones_like(x), transpose_b=True),
}
s = tf.Session()
results = {
    key: np.array([s.run(val, feed_dict={x:r}) for j in range(N)])
    for key, val in examples.items()
}
for key, val in results.items():
    print('%20s mean = %.8f max-min = %.6f' % (key, val.mean(), val.max() - val.min()))</code></pre><p>If you don't want to switch runtimes to see the above output, you can just trust that the <code>reduce_sum</code> version produces inconsistencies as large as the hundredths place, while the <code>matmul</code> version is deterministic at least out to the millionths. I reproduced the above code block in TensorFlow 2.x calling TensorFlow 1.x syntax, but they seem to have changed the way <code>tf.reduce_sum()</code> gets implemented on the backend even in the naive call, as the outputs for the below are identical:</p><pre><code>import tensorflow as tf
import numpy as np
N = 100
S = (1, 100000)
np.random.seed(1)
r = np.random.normal(0, 100, S).astype(np.float32)

def run_graph():
    tf.compat.v1.disable_eager_execution()
    
    x = tf.compat.v1.placeholder(tf.float32, S)
    examples = {
        'reduce_sum': tf.reduce_sum(x),
        'reduce_sum_det': tf.matmul(x, tf.ones_like(x), transpose_b=True),
    }
    
    with tf.compat.v1.Session() as s:
        results = {
            key: np.array([s.run(val, feed_dict={x: r}) for j in range(N)])
            for key, val in examples.items()
        }
    
    return results

results = run_graph()

# Print the results
for key, val in results.items():
    print('%20s mean = %.8f max-min = %.6f' % (key, val.mean(), val.max() - val.min()))

## So just by upgrading our version of TensorFlow we can achieve much greater determinism with no code changes!</code></pre><p>If you want to dig further into the above, I would strongly advise you to check out <a href="https://www.twosigma.com/articles/a-workaround-for-non-determinism-in-tensorflow/">A Workaround for Non-Determinism in TensorFlow</a> since they provide additional example code that extends this idea from the weights to the bias terms and shows a fully deterministic training run for a neural network on the MNIST dataset.</p><p>For our purposes we've already arrived at an intermediate answer: due to the non-associative properties of floating point arithmetic, certain operations heavily leveraged by deep learning algorithms are implemented at the hardware level in a bit-wise deterministic manner while others are not.</p><p>Unfortunately we can't stop here. Simply leveraging operations like <code>tf.matmul()</code> instead of atomic operations like the old version of <code>tf.reduce_sum()</code> reduces the efficiency of deep learning models substantially as the number of GPUs is increased. For LLMs especially, where hundreds of GPUs may be used, the cost impact of a deterministic architecture would be substantial. Add to that any or all of the following:</p><ul><li><p>There could be multiple GPU types the model runs on</p></li><li><p>Other operations such as attention masks, dropout, and different sampling methods also exist</p></li><li><p>In multi-GPU settings timing in synchronization can change results</p></li><li><p>Additional problems likely not considered</p></li></ul><p>Even after setting fixed seeds, disabling certain stochastic optimizations, and controlling the data flow using something like <a href="https://docs.mosaicml.com/projects/streaming/en/latest/preparing_datasets/dataset_format.html">MDS</a>, we're seemingly still back to square 1, with non-deterministic LLMs.</p><h3><strong>Enter our Second Protagonist: LlamaForCausalLM</strong></h3><p><a href="https://huggingface.co/docs/transformers/main/en/model_doc/llama#transformers.LlamaForCausalLM">LlamaForCausalLM</a> has a parameter called <code>do_sample</code>, which if set to <code>False</code> results in no temperature scaling, no top-k or top-p filtering, and no random sampling. In theory, the computation flow reduces to:</p><p><em>Forward pass -&gt; get logits -&gt; argmax -&gt; pick single highest token -&gt; rinse and repeat</em></p><p>And we can test this out for ourselves on Databricks by importing it from the <code>transformers</code> library.</p><pre><code>## Setting up our two examples with a small model and a basic prompt:
import numpy as np
import pandas as pd
import random
import torch
from transformers import pipeline, set_seed, AutoTokenizer, LlamaForCausalLM

# HuggingFace token needed, Llama 3 is a gated repo
hf_token = dbutils.secrets.get(scope="austin_zaccor", key="hf_read_token")
model_id = "meta-llama/Llama-3.2-1B"

# Load tokenizer and model
tokenizer = AutoTokenizer.from_pretrained(model_id, token=hf_token)
model = LlamaForCausalLM.from_pretrained(
    model_id,
    torch_dtype=torch.float32,
    token=hf_token
)

prompt = "Explain the concept of determinism in large language models."</code></pre><pre><code>## Non-deterministic version first:
non_deterministic_results = []
for i in range(5):
  inputs = tokenizer(prompt, return_tensors='pt')
  outputs = model.generate(
    inputs.input_ids, 
    max_length=100, 
    do_sample=True,
    temperature=None, ## unsetting temperature
    top_p=None,       ## unsetting top_p
    pad_token_id=tokenizer.eos_token_id
  )
  non_deterministic_results.append(tokenizer.decode(outputs[0]))

for result in non_deterministic_results:
  print(result, '\n\n')</code></pre><pre><code>## Identical to the above, except do_sample=False
deterministic_results = []
for i in range(5):
  inputs = tokenizer(prompt, return_tensors='pt')
  outputs = model.generate(
    inputs.input_ids, 
    max_length=100, 
    do_sample=False,
    temperature=None, ## unsetting temperature
    top_p=None,       ## unsetting top_p
    pad_token_id=tokenizer.eos_token_id
  )
  deterministic_results.append(tokenizer.decode(outputs[0]))

for result in deterministic_results:
  print(result, '\n\n')</code></pre><p>Now we're back to something that looks promising. The first five are all over the place while the second five are all identical down to the token! In practice, this will create functionally deterministic outputs for most use cases, but the stochastic factors present in the typical forward pass are still present in our reduced computational flow:</p><p><em>Forward pass -&gt; get logits -&gt; argmax -&gt; pick single highest token -&gt; rinse and repeat</em></p><p>Therefore, as we expand the output tokens, otherwise negligable differences in floating point calculations can get magnified, resulting in eventual deviations from true determinism. For example, it's possible that in applications where the top two or more logit values are extremely similar, small differences in floating point calculations in the forward pass will be enough to flip the selected token in the argmax step. This would then "bioaccumulate" through the remaining autoregressive token calculations until you have a very different output.</p><p>Hopefully, this level of reproducibility is still sufficient for your use case, but if not, I hope you will create the world's first truly deterministic implementation of a major LLM.</p><p>Before closing, I want to make a remark on the difference between setting <code>temp=0.0</code> and LlamaForCausalLM's <code>do_sample=False</code>. In theory, both should result in the same greedy decoding and therefore the same outputs, but this is empirically not the case. Databricks users deploying a Llama 3 model will naturally want to do so via <a href="https://docs.databricks.com/aws/en/machine-learning/foundation-model-apis/deploy-prov-throughput-foundation-model-apis">Provisioned Throughput</a> to take advantage of the significant performance improvements it offers over custom GPU model serving. However, Provisioned Throughput endpoints do not allow you to set <code>do_sample=False</code>. The most you can do is set <code>temp=0.0</code>, and doing so will reveal that difference.</p><p>As we discussed in the first half of this blog, determinism can sometimes come at the cost of performance, especially in multi-GPU environments. It is therefore unsurprising that the Provisioned Throughput framework favors speed over strict reproducibility. For now, I would advise those with Llama-based use cases that are highly sensitive to non-determinism to use custom GPU model serving if it can accomodate the size of your LLM and SLAs. For everyone else, take comfort in knowing that deterministic outputs are usually less accurate than their stochastic counterparts due to the greedy decoding that underlies them.</p><p>Happy coding.</p><div><hr></div><p></p><p><strong>About Me:</strong></p><p>I come from a DS/ML background, which I did for about 6 years before starting at Databricks as a Specialist Solutions Architect in GenAI and MLOps. I like to write about things I find interesting and that I think other people might benefit from.</p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[A Beginner's Guide to MLOps Stacks on Databricks]]></title><description><![CDATA[Equipped with an almost excessive amount of diagrams!]]></description><link>https://www.databricksters.com/p/a-beginners-guide-to-mlops-stacks</link><guid isPermaLink="false">https://www.databricksters.com/p/a-beginners-guide-to-mlops-stacks</guid><dc:creator><![CDATA[Veena]]></dc:creator><pubDate>Tue, 18 Feb 2025 18:00:15 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!SCCU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>MLOps Stacks is a template using Databricks Asset Bundles (aka DABs) to implement an MLOps workflow. It is easily customizable, but if you are not familiar with DABs or MLOps, it can get overwhelming quite quickly. There are a lot of folders. A lot of files. But by the end of this blog, you will understand how to use this template for your own use case. </p><p>Instantiating your first MLOps Stack is quite easy. <a href="https://docs.databricks.com/en/dev-tools/bundles/mlops-stacks.html">I would recommend creating a basic one using the instructions here and walk through this blog.</a> You can also take a look at the template directly in the public Github repository: <a href="https://github.com/databricks/mlops-stacks/tree/main/template/%7B%7B.input_root_dir%7D%7D">databricks/mlops-stacks</a>. </p><p>Your project bundle is controlled by the <code>databricks.yml</code> file. This contains all of the configurations (aka what the STAGE workspace is, what the DEV workspace is, what the `prod` catalog is, etc.). This file also points to all of the workflows in the project. Surprise! These configurations are also written in yaml. And the workflow configurations point to the notebooks in the project. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!SCCU!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!SCCU!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png 424w, https://substackcdn.com/image/fetch/$s_!SCCU!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png 848w, https://substackcdn.com/image/fetch/$s_!SCCU!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png 1272w, https://substackcdn.com/image/fetch/$s_!SCCU!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!SCCU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png" width="643" height="503" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/d0204993-00b5-42fa-b443-133029861c71_643x503.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:503,&quot;width&quot;:643,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:36128,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!SCCU!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png 424w, https://substackcdn.com/image/fetch/$s_!SCCU!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png 848w, https://substackcdn.com/image/fetch/$s_!SCCU!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png 1272w, https://substackcdn.com/image/fetch/$s_!SCCU!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd0204993-00b5-42fa-b443-133029861c71_643x503.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>This walkthrough follows the &#8220;Deploy code&#8221; approach, which is generally recommended by Databricks. This means that the code moves from development to staging and then production. The model is trained in each environment. However, there are certain scenarios where &#8220;Deploy model&#8221; works better, like if your model training process was quite expensive. This is why the &#8220;Deploy model&#8221; approach is more common in LLMOps, but more on this in a later blog post. </p><h2>Let&#8217;s walk through a theoretical example of how this would work. </h2><h3>Development</h3><h4>Step One: Exploratory Data Analysis</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AnjR!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AnjR!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png 424w, https://substackcdn.com/image/fetch/$s_!AnjR!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png 848w, https://substackcdn.com/image/fetch/$s_!AnjR!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png 1272w, https://substackcdn.com/image/fetch/$s_!AnjR!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AnjR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png" width="958" height="936" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:936,&quot;width&quot;:958,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:71193,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!AnjR!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png 424w, https://substackcdn.com/image/fetch/$s_!AnjR!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png 848w, https://substackcdn.com/image/fetch/$s_!AnjR!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png 1272w, https://substackcdn.com/image/fetch/$s_!AnjR!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4a50c8e9-4315-489a-b7a6-4e258987958b_958x936.png 1456w" sizes="100vw"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>In the DEV environment, we explore new data alongside existing production data stored in the `dev` catalog. Perhaps, during this exploration, there is a discovery! There is a eureka moment! Now, we need to train and tune a new model. </p><h4>Step Two: Model Training</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!sLQo!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!sLQo!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png 424w, https://substackcdn.com/image/fetch/$s_!sLQo!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png 848w, https://substackcdn.com/image/fetch/$s_!sLQo!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png 1272w, https://substackcdn.com/image/fetch/$s_!sLQo!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!sLQo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png" width="980" height="940" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:940,&quot;width&quot;:980,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:82410,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!sLQo!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png 424w, https://substackcdn.com/image/fetch/$s_!sLQo!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png 848w, https://substackcdn.com/image/fetch/$s_!sLQo!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png 1272w, https://substackcdn.com/image/fetch/$s_!sLQo!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb0cf4417-e512-492f-914b-a7d269d5d2cf_980x940.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>We are still in the DEV environment, but now we can use MLFlow to keep track of everything. If you are unfamiliar with MLFlow, I would recommend taking a look at this <a href="https://docs.databricks.com/en/_extras/notebooks/source/mlflow/mlflow-end-to-end-example-uc.html">demo notebook linked here</a>. But shortly, MLFlow is a way for you to track your experiments and within that, your runs (each run = iteration of training). If that concept is still confusing, <a href="https://mlflow.org/docs/latest/traditional-ml/hyperparameter-tuning-with-child-runs/part1-child-runs.html">take a look at the docs here</a>.  </p><p>Using MLFlow, we can log key metrics, parameters, and artifacts across different runs, enabling us compare and contrast different trained models and pick the best model. Finally, once we are satisfied with the model that we have created, we can register the model in the `dev` catalog. </p><h4>Step Three: Push Changes</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Paso!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Paso!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png 424w, https://substackcdn.com/image/fetch/$s_!Paso!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png 848w, https://substackcdn.com/image/fetch/$s_!Paso!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png 1272w, https://substackcdn.com/image/fetch/$s_!Paso!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Paso!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png" width="1084" height="674" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:674,&quot;width&quot;:1084,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:53501,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!Paso!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png 424w, https://substackcdn.com/image/fetch/$s_!Paso!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png 848w, https://substackcdn.com/image/fetch/$s_!Paso!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png 1272w, https://substackcdn.com/image/fetch/$s_!Paso!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe84117a1-ab5e-4a87-9028-82d98b009a1c_1084x674.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Let&#8217;s update the repository now! Within a Databricks workspace, we can create a temporary branch (here we call it `dev`) and merge the updated notebooks. </p><p>Note: if you are following along via the MLOps Stacks template, you can see that there is a logical separation in the template structure: </p><ul><li><p>feature_engineering</p><ul><li><p>contains feature transformations</p></li><li><p>In this example, we are using the Databricks Feature Store, which is a centralized repository for managing and serving features</p></li></ul></li><li><p>monitoring</p><ul><li><p>contains code for model monitoring</p></li><li><p>we separate this from validation as monitoring is on-going in production while validation occurs pre-deployment. In this template, <code>ModelValidation.py </code>is the second part of the Model Training workflow</p></li></ul></li><li><p>validation</p><ul><li><p>contains code for model validation</p></li></ul></li><li><p>deployment</p><ul><li><p>contains serving endpoint setup and configuration</p></li></ul></li><li><p>training</p><ul><li><p>contains model training logic (e.g. all of the MLFlow)</p></li></ul></li></ul><p>This separation follows the principal of separation of concerns&#8212; each directory has a specific responsibility in the ML lifecycle. It also makes it easier to: </p><ul><li><p>have different teams work on different aspects of the lifecycle</p></li><li><p>maintain and update specific parts of the pipeline</p></li><li><p>reuse components across different projects</p></li><li><p>implement proper testing of each component</p></li></ul><p>And now, after the branch is created, we can commit the code directly to the `dev` branch. This will now move us to the next part of the process. </p><h3>Staging</h3><h4>Step One: Pull Request</h4><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lcvZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lcvZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png 424w, https://substackcdn.com/image/fetch/$s_!lcvZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png 848w, https://substackcdn.com/image/fetch/$s_!lcvZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png 1272w, https://substackcdn.com/image/fetch/$s_!lcvZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lcvZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png" width="1456" height="244" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:244,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:77299,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!lcvZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png 424w, https://substackcdn.com/image/fetch/$s_!lcvZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png 848w, https://substackcdn.com/image/fetch/$s_!lcvZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png 1272w, https://substackcdn.com/image/fetch/$s_!lcvZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3a96fa94-e276-4b0d-a8c7-b77c2ae0b23c_2168x364.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Once our code is merged into the `dev` branch, we can open a Pull Request to merge the new changes to our `main` branch. It is time to test all of these changes. </p><p>Databricks Asset Bundles allows you to easily maintain Databricks resources, but you will still need a way to automate and run the workflows. I have personally used a lot of Github Actions, but you can set up Azure DevOps Pipelines, GitLab Pipelines, etc. </p><h4>Step Two: Unit and Integration Tests</h4><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!FvEZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!FvEZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png 424w, https://substackcdn.com/image/fetch/$s_!FvEZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png 848w, https://substackcdn.com/image/fetch/$s_!FvEZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png 1272w, https://substackcdn.com/image/fetch/$s_!FvEZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!FvEZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png" width="1456" height="876" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:876,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:141514,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!FvEZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png 424w, https://substackcdn.com/image/fetch/$s_!FvEZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png 848w, https://substackcdn.com/image/fetch/$s_!FvEZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png 1272w, https://substackcdn.com/image/fetch/$s_!FvEZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F560b2d36-0f95-43ec-97c5-88731effea79_1718x1034.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Opening a PR immediately triggers multiple workflows (via Github Actions or whatever process you have set up) for Unit Tests and Integration Tests. </p><p>The Integration Tests workflow (<a href="https://github.com/databricks/mlops-stacks/blob/main/template/%7B%7B.input_root_dir%7D%7D/.github/workflows/%7B%7B.input_project_name%7D%7D-run-tests.yml.tmpl">link to Github Actions workflow</a>) uses Databricks Asset Bundle (DAB) commands to create the assets in the Staging environment and trigger the workflow to run the necessary notebooks: </p><pre><code><code># Validate the bundle configuration
databricks bundle validate -t test 

# Creates all necessary assets in stage environment
databricks bundle deploy -t test

# Executes feature engineering pipeline
databricks bundle run write_feature_table_job -t test 

# Executes model training pipeline
databricks bundle run model_training_job -t test  </code></code></pre><p>The `-t test` flag specifies that these commands should target the STAGE environment, as defined in the <code>databricks.yml</code> file. </p><p>We are running two jobs: the feature engineering workflow (<a href="https://github.com/databricks/mlops-stacks/blob/main/template/%7B%7B.input_root_dir%7D%7D/%7B%7Btemplate%20%60project_name_alphanumeric_underscore%60%20.%7D%7D/resources/feature-engineering-workflow-resource.yml.tmpl">link</a>), which computes all of the features and stores them in the Databricks Feature Store, and the model training workflow (<a href="https://github.com/databricks/mlops-stacks/blob/main/template/%7B%7B.input_root_dir%7D%7D/%7B%7Btemplate%20%60project_name_alphanumeric_underscore%60%20.%7D%7D/resources/model-workflow-resource.yml.tmpl">link</a>), which trains the model with the Feature Store and then validates the model. </p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cyv5!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cyv5!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png 424w, https://substackcdn.com/image/fetch/$s_!cyv5!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png 848w, https://substackcdn.com/image/fetch/$s_!cyv5!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png 1272w, https://substackcdn.com/image/fetch/$s_!cyv5!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cyv5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png" width="1348" height="1144" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/785d52d9-fea4-408d-b299-252491806129_1348x1144.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1144,&quot;width&quot;:1348,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:131040,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!cyv5!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png 424w, https://substackcdn.com/image/fetch/$s_!cyv5!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png 848w, https://substackcdn.com/image/fetch/$s_!cyv5!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png 1272w, https://substackcdn.com/image/fetch/$s_!cyv5!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F785d52d9-fea4-408d-b299-252491806129_1348x1144.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>These test can be expanded even further to cover all aspects of the model workflows: </p><ul><li><p>Feature Engineering tests</p><ul><li><p>verify the data transformation pipeline</p></li><li><p>For example, test that the data types are expected, the missing values are handled properly, the feature values are within an expected range, and the Databricks Feature Store itself is working properly</p></li></ul></li><li><p>Model Training tests</p><ul><li><p>verify the model training process</p></li><li><p>For example, ensure that the resources are utilized properly; metrics and parameters are correctly logged in MLFlow; and the model can be saved and loaded</p></li></ul></li><li><p>Model Validation tests</p><ul><li><p>verify the model&#8217;s performance and behavior</p></li><li><p>For example, make sure the model predictions are expected, compare the model&#8217;s performance against previous versions, and check if the model is meeting the expected thresholds </p></li></ul></li><li><p>Model Deployment tests</p><ul><li><p>verify that the model can be deployed and served</p></li><li><p>For example, test performance under expected traffic</p></li></ul></li><li><p>Model Inference tests</p><ul><li><p>verify the model&#8217;s behavior during prediction</p></li><li><p>For example, ensure that the inference speeds meets requirements, monitor resource consumption, and verify that data is being logged to the Inference Tables correctly </p></li></ul></li><li><p>Model Monitoring tests</p><ul><li><p>verify the monitoring system is working</p></li><li><p>For example, test the alerting system and validate the visualizations in the Dashboards</p></li></ul></li></ul><p>After a successful job run, you can successfully merge the `dev` branch to the `main` branch. Let&#8217;s move to the next and final part of the process. </p><h3>Production</h3><h4>Step One: Release Branch</h4><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!n4R6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!n4R6!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png 424w, https://substackcdn.com/image/fetch/$s_!n4R6!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png 848w, https://substackcdn.com/image/fetch/$s_!n4R6!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png 1272w, https://substackcdn.com/image/fetch/$s_!n4R6!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!n4R6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png" width="1456" height="212" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:212,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:92256,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!n4R6!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png 424w, https://substackcdn.com/image/fetch/$s_!n4R6!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png 848w, https://substackcdn.com/image/fetch/$s_!n4R6!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png 1272w, https://substackcdn.com/image/fetch/$s_!n4R6!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe4247bfc-6de3-4539-8935-eda961c2d097_2564x374.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>After successfully validating the changes in the STAGE environment, we can now create a `release` branch to update the changes in the PROD environment. This branching strategy allows us to provide a clear snapshot of what is being deployed to production, enables hotfixes if issues arise without disrupting the main branch, and creates a history. </p><p>The creation of a `release` branch triggers a production deployment workflow in your CI/CD system. </p><h4>Step Two: Create Assets</h4><p>This is similar to what happened before: </p><pre><code><code># Validate the bundle configuration 
databricks bundle validate -t prod

# Deploy assets the PROD environment
databricks bundle deploy -t prod</code></code></pre><p>Instead of running any specific jobs, like we did in the STAGE environment, we are simply deploying the assets (<a href="https://github.com/databricks/mlops-stacks/blob/main/template/%7B%7B.input_root_dir%7D%7D/.github/workflows/%7B%7B.input_project_name%7D%7D-bundle-cd-prod.yml.tmpl">link to Github Actions workflow</a>) because they are scheduled. This schedule is defined in each workflow configuration.  </p><p>Now, with assets deployed to production and workflow scheduled, we have completed the full MLOps lifecycle implementation. </p><h2>Next Steps</h2><p>Ready to start implementing?</p><ol><li><p>Create your first MLOps Stack using the template repository. </p></li><li><p>Review the example notebooks in the template and understand how each component works. </p></li><li><p>Adapt the workflow configurations to match your needs. </p></li><li><p>Set up your CI/CD pipeline using Github Actions or your preferred tool. </p></li></ol><p>In future posts, we will dive deeper on LLMOps and Best Practices. If you have any questions about implementing MLOps Stacks, feel free to reach out via the comments. </p><p>I will leave you with a poll: </p><div class="poll-embed" data-attrs="{&quot;id&quot;:275053}" data-component-name="PollToDOM"></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/p/a-beginners-guide-to-mlops-stacks?utm_source=substack&utm_medium=email&utm_content=share&action=share&quot;,&quot;text&quot;:&quot;Share&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.databricksters.com/p/a-beginners-guide-to-mlops-stacks?utm_source=substack&utm_medium=email&utm_content=share&action=share"><span>Share</span></a></p><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Deploying DeepSeek R1 Distill Qwen 1.5B on Databricks]]></title><description><![CDATA[&#8220;The results are promising: DeepSeek-R1-Distill-Qwen-1.5B outperforms GPT-4o and Claude-3.5-Sonnet on math benchmarks with 28.9% on AIME and 83.9% on MATH.]]></description><link>https://www.databricksters.com/p/deploying-deepseek-r1-distill-qwen</link><guid isPermaLink="false">https://www.databricksters.com/p/deploying-deepseek-r1-distill-qwen</guid><dc:creator><![CDATA[Austin]]></dc:creator><pubDate>Tue, 04 Feb 2025 16:01:47 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!nTbs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="pullquote"><p>&#8220;The results are promising: DeepSeek-R1-Distill-Qwen-1.5B outperforms GPT-4o and Claude-3.5-Sonnet on math benchmarks with 28.9% on AIME and 83.9% on MATH. Other dense models also achieve impressive results, significantly outperforming other instruction-tuned models based on the same underlying checkpoints.&#8221;&#8202;&#8212;&#8202;DeepSeek-AI</p></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nTbs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nTbs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg 424w, https://substackcdn.com/image/fetch/$s_!nTbs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg 848w, https://substackcdn.com/image/fetch/$s_!nTbs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!nTbs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nTbs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg" width="1179" height="659" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:659,&quot;width&quot;:1179,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:325013,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nTbs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg 424w, https://substackcdn.com/image/fetch/$s_!nTbs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg 848w, https://substackcdn.com/image/fetch/$s_!nTbs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!nTbs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68fd5aeb-c20e-44fc-be7b-c36ff9be3ab8_1179x659.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption"><a href="https://x.com/NotPotBol/status/1883902668864938123">Not Potato Bolshevik&#8217;s Tweet</a></figcaption></figure></div><p>New LLMs usually make waves in the AI spheres, but DeepSeek R1 was more of a tsunami this week as the US stock market lost about a trillion dollars in valuation inside of 30 minutes. Regardless of your opinions on DeepSeek and its implications for the global (or national) economy, it&#8217;s an open source model whose performance is very competitive with the largest proprietary models currently available. That performance is due both to architecture it shares in common with large proprietary reasoning models, as well as some very interesting departures from the current training paradigm of ChatGPT, Claude, and Gemini. (<a href="https://openai.com/index/openai-o3-mini/">At least until very recently</a>).</p><p>For starters, <code>DeepSeek-R1</code> comes in a few flavors, and it's worth understanding the significance of each:</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><ul><li><p><strong>DeepSeek-R1-Zero</strong></p><ul><li><p>Applies RL directly to the base model without any Supervised Fine Tuning (SFT) data at all</p></li></ul></li><li><p><strong>DeepSeek-R1</strong></p><ul><li><p>Applies RL starting from a checkpoint fine-tuned with minimal high quality Chain of Thought (CoT) examples</p></li></ul></li><li><p><strong>DeepSeek-R1-Distill-X</strong></p><ul><li><p>A half dozen smaller, dense models distilled from DeepSeek-R1 that preserve the reasoning patterns learned from its much larger parent model</p></li></ul></li></ul><p>The <a href="https://arxiv.org/html/2501.12948v1#S1">DeepSeek Paper</a> explains how using 800k high quality fine-tuning samples curated from DeepSeek-R1 is enough data to significantly improve the reasoning abilities of smaller models such as Qwen2.5, to the point where even just the 1.5B variant can outperform a model with orders of magnitude more parameters on math tasks. Notably, this distillation process is purely SFT based, and includes no RL at all.</p><p>So how can we host one of these overpowered mini models ourselves? Databricks recently published <a href="https://www.databricks.com/blog/deepseek-r1-databricks">a blog post</a> showing how to deploy DeepSeek R1 Distill Llama 8B and 70B using Provisioned Throughput directly on the Databricks platform, which makes sense given that Llama 3.x architecture is natively supported, even for fine tuned variants. However, what if we want to serve one of the four Qwen2.5 based distilled models? The architectural differences between Llama 3 and Qwen2.5 are so small you can actually <a href="https://github.com/hiyouga/LLaMA-Factory/blob/main/scripts/convert_ckpt/llamafy_qwen.py">convert one to the other</a>, but if you don&#8217;t want to convert all the weights to Llama, you&#8217;re going to need to use custom GPU Model Serving for Qwen.</p><p>And that brings us to the code portion of this blog:</p><pre><code>%pip install accelerate
%pip install transformers --upgrade ## need RoPE for this to work, and that's only included in newer versions
%pip install torch --upgrade        ## torch version also matters here
%pip uninstall torch torchvision -y
%pip install torch torchvision --index-url https://download.pytorch.org/whl/cu124

dbutils.library.restartPython()</code></pre><pre><code>import torch
import torchvision
import accelerate
import transformers

## Feel free to compare to the conda_env specified below to double check
print(transformers.__version__)
print(accelerate.__version__)
print(torch.__version__)
print(torchvision.__version__)</code></pre><pre><code>import pandas as pd
import mlflow
import mlflow.transformers
from mlflow.models.signature import infer_signature
from transformers import AutoModelForCausalLM, AutoTokenizer, AutoConfig, pipeline

# Enable MLflow Autologging
mlflow.set_tracking_uri("databricks")

# Specify the model from HuggingFace transformers
model_name = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"

# Load tokenizer and model
tokenizer = AutoTokenizer.from_pretrained(model_name)
config = AutoConfig.from_pretrained(model_name)

# Adjust rope_scaling
if "rope_scaling" in config.to_dict():
    config.rope_scaling = {"type": "dynamic", "factor": 8.0}

# Loading this on single node, single GPU A10 cluster, for 14B or 32B models you will need multiple GPUs of this size
model = AutoModelForCausalLM.from_pretrained(
    model_name, 
    config=config, 
    device_map="cuda:0"
)

text_generator = pipeline("text-generation", model=model, tokenizer=tokenizer)

# We need a signature for UC registered models
example_prompt = "Explain quantum mechanics in simple terms."
example_inputs = pd.DataFrame({"inputs": [example_prompt]})
example_outputs = text_generator(example_prompt, max_length=200)
signature = infer_signature(example_inputs, example_outputs)

# Define the Conda environment with correct package versions
conda_env = {
    "name": "mlflow-env",
    "channels": ["defaults", "conda-forge"],
    "dependencies": [
        "python=3.11",
        "pip",
        {
            "pip": [
                "mlflow",
                "transformers==4.48.1",
                "accelerate==0.31.0",
                "torch==2.6.0",
                "torchvision==0.21.0"
            ]
        }
    ]
}

# Log model with MLflow
with mlflow.start_run() as run:
    mlflow.transformers.log_model(
        transformers_model=text_generator,
        artifact_path="deepseek_model",
        signature=signature,
        input_example=example_inputs,
        registered_model_name="deepseek_qwen_1_5b",
        conda_env=conda_env
    )</code></pre><p>The above is all you need log and register the model to MLflow! From here I like to perform two tests before I kick off a serving endpoint, because I want to be reasonably certain I won&#8217;t get a container build failure before I wait 30 minutes for a GPU model serving endpoint to fully spin up:</p><ol><li><p>I test the model using <code>load_model()</code></p><ol><li><p>This catches immediate errors with my class&#8217;s logic and is quite fast, but doesn&#8217;t test the dependencies for compatibility issues because it&#8217;s loading it in the same notebook environment we kicked if off from</p></li></ol></li><li><p>I test the model again using <code>mlflow.models.predict()</code></p><ol><li><p>This catches the dependency issues in my <code>conda_env</code> because it spins up a lightweight virtual env that mimics the serving endpoint</p></li></ol></li></ol><p>If both pass, then I go to the experiment in MLflow and deploy the model to a live endpoint.</p><pre><code># Load the model locally for test 1
model_uri = "models:/deepseek_qwen_1_5b/4"
loaded_model = mlflow.pyfunc.load_model(model_uri)

input_data = {"inputs": "Explain quantum mechanics in simple terms."}
output = loaded_model.predict(input_data)
print(output)</code></pre><pre><code>## Call model in virtual env as specified by conda_env for test 2
# Define the model URI
model_uri = "models:/deepseek_qwen_1_5b/4"

# Define input data in the required format
input_data = pd.DataFrame({"inputs": ["Explain quantum mechanics in simple terms."]})

# Call the MLflow model predict API since that's better than load_model()
output = mlflow.models.predict(model_uri, input_data)
print(output)</code></pre><p>Now we grab some tea and wait for DeepSeek-R1-Distill-Qwen-1.5B to deploy!</p><div><hr></div><p><strong>About Me:</strong></p><p>I come from a DS/ML background, which I did for about 6 years before starting at Databricks as a Specialist Solutions Architect in GenAI and MLOps. I like to write about things I find interesting and that I think other people might benefit from. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.databricksters.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading The Databricksters! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>