In this article, you will learn what model distillation is, how it has evolved for large language models, and why it has become one of the most contested topics in the AI industry. Topics we will cover include: How classical distillation works, including the concept of “dark knowledge” and temperature scaling. How modern distillation techniques have adapted for large language models, covering synthetic data, feature, and logit-based approaches. Why unauthorized distillation at scale has triggered major industry controversy and what the structural tensions driving it mean for the future. Let’s not waste any more time. Introduction The most capable AI models in the world are also among the most impractical. Models like GPT-4, Claude, and Gemini contain hundreds of billions of parameters and require entire data centers to run. They are expensive, slow, and impossible to deploy on a smartphone, a browser, or any environment where computing resources are limited. This creates a genuine problem: how do you take a model that cost hundreds of millions of dollars to train and make it available to the world at a reasonable cost and speed? One of the most effective answers is model distillation, a technique where a smaller “student” model learns to replicate the behavior of a much larger “teacher” model. The student ends up a fraction of the size but retains a surprising amount of the teacher’s capability. This article explains how that works, how the approach has evolved for modern large language models, and why it has become one of the most contested topics in the AI industry. What a Model Actually Learns To understand distillation, it helps to start with how a standard model learns. In a conventional image classification task, a model trains on labeled photos. Each photo comes with a hard label: this is a dog, that is a cat. The model adjusts its parameters until it reliably predicts the correct label. The training signal is binary: right or wrong. The problem is that hard labels discard a lot of information. A photo of a golden retriever does not just contain “dog” information. It also contains information that dogs look somewhat like cats, that both look nothing like cars, and that certain dog breeds are more visually ambiguous than others. None of that relational structure appears in a simple label. A trained teacher model, however, has already absorbed this structure. When it looks at that photo of a golden retriever, it does not just output “dog.” It outputs a probability distribution: perhaps 85% dog, 13% cat, 2% wolf, near-zero for everything else. These distributions reflect the model’s learned sense of similarity between concepts. Geoffrey Hinton, who developed the foundational distillation framework, called the information embedded in these distributions “dark knowledge” — knowledge that exists in the model but is invisible in the raw labels. How Classical Distillation Works The central insight of distillation is simple: instead of training the student on hard labels, train it on the teacher’s probability distributions. The student learns not just what the right answer is, but the teacher’s full sense of which wrong answers are more plausible than others. This is a richer signal, and it transfers generalization capability in a way that hard labels cannot. There is one practical complication. At normal confidence, a teacher model tends to produce very peaked distributions, assigning 99% probability to the correct class and near-zero to everything else. That distribution is barely more informative than a hard label. To expose the subtle relationships between classes, distillation uses a technique called temperature scaling. Increasing the temperature “softens” the distribution, flattening it out so the smaller differences between classes become visible. The student trains on these softened distributions. The student also trains against the original ground-truth labels at the same time. The final training objective is a blend of two goals: match the teacher’s soft outputs, and get the actual answers right. A single weighting parameter controls the balance between the two. In practice, heavier weight on the teacher’s soft distributions tends to produce better results. Hinton, Vinyals, and Dean published this framework in 2015 and demonstrated it on speech recognition and image classification. Small distilled models were able to match the performance of much larger model ensembles, suggesting that the teacher’s dark knowledge was being successfully transferred. Modern Distillation: How Large Language Models Do It Classical distillation works well for tasks with a fixed set of output classes. Large language models present a different challenge. They generate text token by token across vocabularies that can exceed 100,000 tokens. The probability distribution at each step is enormous, and the structure of the problem is fundamentally sequential rather than categorical. The classical framework does not transfer cleanly. Modern distillation has adapted into three main approaches. Synthetic data distillation is now the dominant method. Rather than matching probability distributions, the teacher generates large volumes of high-quality text, which the student then learns from directly. Step-by-step reasoning chains, worked examples, code solutions, structured analysis: the teacher produces these at scale, and the student is fine-tuned on this synthetic dataset. This approach requires only access to the teacher’s text outputs, not its internal architecture or weights. Feature distillation takes a different path. Rather than matching the teacher’s final outputs, the student learns to replicate the teacher’s internal representations — the patterns of activation at intermediate layers. This transfers a deeper structural understanding of how the teacher processes information, but it requires full access to the teacher’s architecture. It is typically used when an organization is distilling its own models. Logit-based distillation applies a version of the classical framework at the token level, matching the teacher’s full token probability distributions rather than just the sampled text. This also requires white-box access to the teacher’s internals and is used primarily in-house. The distinction between these approaches has significant practical consequences. Synthetic data distillation requires only API access to the teacher’s text outputs, which means it can be applied to any model that offers a public interface. Feature and logit-based distillation require
Access Denied
Access Denied You don’t have permission to access “http://zeenews.india.com/technology/opinion-the-power-of-ai-guided-by-spirituality-3072145.html” on this server. Reference #18.5cfdd417.1789765374.13f4a7f4 https://errors.edgesuite.net/18.5cfdd417.1789765374.13f4a7f4
Access Denied
Access Denied You don’t have permission to access “http://zeenews.india.com/technology/huge-queues-form-at-apple-stores-as-iphone-18-series-goes-on-sale-nationwide-3072113.html” on this server. Reference #18.c4f43717.1789707995.67c7acc1 https://errors.edgesuite.net/18.c4f43717.1789707995.67c7acc1
Access Denied
Access Denied You don’t have permission to access “http://zeenews.india.com/technology/iqoo-16-launch-confirmed-for-sept-29-unveiling-world-first-165hz-2k-display-robotic-design-massive-battery-3071947.html” on this server. Reference #18.7092e17.1789661673.70c7fdf0 https://errors.edgesuite.net/18.7092e17.1789661673.70c7fdf0
Dataclasses for Structured Application Data
In this article, you will learn how Python’s dataclass decorator can replace fragile configuration dictionaries with structured, readable, and maintainable data models. Topics we will cover include: How to build and compose dataclasses for real application configurations, including handling mutable defaults and nested records. How to enforce local invariants at construction time using __post_init__, and how to express immutability with frozen=True. How to serialize and deserialize dataclasses at JSON boundaries deliberately, and when to reach for a heavier tool like Pydantic instead. The configuration dictionary in your batch job probably works fine today. It worked fine last month too, which is exactly how it accumulated a misspelled key nobody noticed and an optional field that two call sites default differently. Somewhere in there is also a nested dictionary whose shape depends on which function built it. The dictionary didn’t fail loudly; it let three parts of the application disagree quietly, and the disagreement only surfaces when a routine change lands on the wrong assumption. config = { “batch_size”: 500, “max_attempts”: 3, “output”: {“format”: “parquet”, “compress”: True}, } # …three modules away size = config.get(“batchsize”, 100) # typo: silently runs with 100 config = { “batch_size”: 500, “max_attempts”: 3, “output”: {“format”: “parquet”, “compress”: True}, } # …three modules away size = config.get(“batchsize”, 100) # typo: silently runs with 100 Python’s standard library has had a better tool for this since 3.7, and it asks for almost nothing in return. Decorate a class with @dataclass, annotate the fields, and the dataclasses module generates the initializer, representation, and equality methods for you. One boundary needs stating before anything else, though, because it shapes every design decision in this article: those field annotations describe the model, but the generated code does not check them at runtime. A dataclass is a contract you can read, not a validator that enforces itself. What that contract buys you, where its edges are, and when to reach for a heavier tool is what the rest of this article works through, using one batch-processing job that grows the way real application code does. Start With the Smallest Useful Data Model Here’s the loose dictionary’s replacement in its minimal form: from dataclasses import dataclass @dataclass class JobConfig: name: str batch_size: int = 500 job = JobConfig(“nightly-import”) print(job) # JobConfig(name=”nightly-import”, batch_size=500) print(job == JobConfig(“nightly-import”)) # True from dataclasses import dataclass @dataclass class JobConfig: name: str batch_size: int = 500 job = JobConfig(“nightly-import”) print(job) # JobConfig(name=”nightly-import”, batch_size=500) print(job == JobConfig(“nightly-import”)) # True Three generated methods are doing the work. __init__ accepts the fields in declaration order, __repr__ prints something you’d actually want in a log line, and __eq__ compares by field values rather than identity. None of that is exotic, and that’s the appeal: you’d write the same boilerplate by hand, slightly differently each time, in every project. The typo from the opening also changes character. job.batchsize raises an AttributeError at the line that’s wrong, and your IDE or type checker flags it before the code even runs, because attributes are checkable in a way string keys aren’t. Now the boundary. Run JobConfig(“nightly-import”, batch_size=”lots”) and it constructs happily. As PEP 557 puts it, the decorator uses annotations to discover fields, and the types are otherwise not examined. The string will travel until something downstream does arithmetic on it. Keep that in mind every time a dataclass field looks like a guarantee; it’s documentation with excellent tooling support, and documentation doesn’t stop anyone at runtime. Compose Nested Records Before One Class Becomes Everything Real configurations sprawl, and the failure mode of a growing dataclass is the same as a growing dictionary: one bag holding twenty loosely related fields. Composition keeps each record responsible for one coherent slice. from dataclasses import dataclass, field @dataclass class RetryPolicy: max_attempts: int = 3 backoff_seconds: float = 2.0 @dataclass class OutputConfig: format: str = “parquet” compress: bool = True @dataclass class JobConfig: name: str batch_size: int = 500 retry: RetryPolicy = field(default_factory=RetryPolicy) output: OutputConfig = field(default_factory=OutputConfig) job = JobConfig( name=”nightly-import”, retry=RetryPolicy(max_attempts=5), ) print(job.retry.max_attempts) # 5 print(job.output.format) # ‘parquet’ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 from dataclasses import dataclass, field @dataclass class RetryPolicy: max_attempts: int = 3 backoff_seconds: float = 2.0 @dataclass class OutputConfig: format: str = “parquet” compress: bool = True @dataclass class JobConfig: name: str batch_size: int = 500 retry: RetryPolicy = field(default_factory=RetryPolicy) output: OutputConfig = field(default_factory=OutputConfig) job = JobConfig( name=“nightly-import”, retry=RetryPolicy(max_attempts=5), ) print(job.retry.max_attempts) # 5 print(job.output.format) # ‘parquet’ Notice the construction is explicit. If you pass retry={“max_attempts”: 5} instead, the dataclass will store the dictionary as-is; nothing walks the annotations converting nested dictionaries into nested dataclasses for you. That surprises people who expect ORM-style magic, and it’s worth internalizing early because it comes back at the serialization boundary later. The same composition pattern covers most structured data an application owns. A request object carrying per-run metadata, a dataset record, a model’s hyperparameter block: each is a small class with a readable shape, and nesting them keeps the shape legible as the system grows. Figure 1. Where structure gets added, and which jobs stay explicitly yours at every stage. Sources: Python dataclasses and json documentation; PEP 557. Original diagram created for this article. Treat Defaults as Part of the Contract Scalar defaults work the way you’d expect, and batch_size: int = 500 is all you need. Mutable defaults are where dataclasses make you slow down, deliberately. @dataclass class ProcessingRequest: job: JobConfig tags: list[str] = field(default_factory=list) a = ProcessingRequest(job) b = ProcessingRequest(job) a.tags.append(“rerun”) print(b.tags) # [] — each instance got its own list @dataclass class ProcessingRequest: job: JobConfig tags: list[str] = field(default_factory=list) a = ProcessingRequest(job) b = ProcessingRequest(job) a.tags.append(“rerun”) print(b.tags) # [] — each instance got its own list Write tags: list[str] = [] instead and Python raises a ValueError at class-definition time, refusing the shared mutable default outright. The default_factory callable communicates the actual intent: every instance gets a
Access Denied
Access Denied You don’t have permission to access “http://zeenews.india.com/technology/apple-iphone-duo-vs-samsung-galaxy-z-fold-8-ultra-which-one-should-you-buy-prices-and-specs-compared-3071949.html” on this server. Reference #18.c4f43717.1789626826.5c9b0082 https://errors.edgesuite.net/18.c4f43717.1789626826.5c9b0082
Access Denied
Access Denied You don’t have permission to access “http://zeenews.india.com/technology/a20-pro-vs-a19-pro-here-s-what-s-new-in-apple-s-latest-chip-3071895.html” on this server. Reference #18.14fdd417.1789587556.bcb231c https://errors.edgesuite.net/18.14fdd417.1789587556.bcb231c
Chain of Thought vs. Tree of Thoughts: Which is Best for AI Agents?
In this article, you will learn the key differences between Chain of Thought and Tree of Thoughts prompting, and how each reasoning framework is applied in AI agent systems. Topics we will cover include: How Chain of Thought works as a linear reasoning technique and where its limitations lie. How Tree of Thoughts extends reasoning through branching, evaluation, and backtracking. How AI agents use both frameworks together, matching each to the complexity of the task at hand. Introduction Large language models have a default behavior that works against complex reasoning. They are trained to predict the next most likely token given everything that came before, which means that, left to their own devices, they tend to leap directly from a question to an answer. For simple tasks, this works fine. For anything that requires multiple steps, careful logic, or planning ahead, it tends to fail in ways that look confident and coherent but are quietly wrong. Two techniques have emerged to address this: Chain of Thought and Tree of Thoughts. Both are designed to force a model to reason before it concludes. They share that goal but pursue it in structurally different ways, with different costs, different strengths, and different failure modes. For AI agents, the choice between these approaches is not cosmetic. It shapes what the agent can actually accomplish. The Problem Both Techniques Solve To understand why these techniques exist, it helps to see clearly what happens without them. Ask a language model a straightforward factual question and it will usually answer correctly. Ask it to solve a problem that requires holding several intermediate conclusions in mind, or that has a structure where early mistakes compound into later ones, and the model’s tendency to jump to a fluent-sounding answer becomes a liability. It will produce text that reads like careful reasoning but was not actually generated that way. The appearance of thought is not the same as thought. Both Chain of Thought and Tree of Thoughts work by inserting intermediate steps between input and output. Instead of mapping directly from question to answer, the model generates a sequence of reasoning steps first. The final answer emerges from those steps rather than directly from the input. This simple change in structure produces measurable improvements on tasks involving mathematics, logic, and multi-step planning. The techniques diverge in how those intermediate steps are organized, how many are generated, and what happens when a step turns out to be wrong. Chain of Thought: Linear Reasoning Chain of Thought is the simpler of the two approaches. It asks the model to show its work: to generate a sequence of intermediate reasoning steps before arriving at a final answer. In its most basic form, this can be triggered by something as minimal as appending the phrase “Let’s think step by step” to a prompt. The model, guided by that instruction, produces a chain of reasoning rather than an immediate conclusion. More structured implementations provide explicit step-by-step instructions or use examples to demonstrate the reasoning format expected. The structure is linear. The model moves from the problem statement to step one, from step one to step two, and so on, until it arrives at an answer. Each step follows directly from the previous one. Think of a student working through an algebra problem on paper, writing each line of calculation in sequence. The approach is transparent, auditable, and easy to follow. This linearity is also its core limitation. If the model makes an error at an early step, that error propagates forward. Every subsequent step is built on a flawed foundation, and the final answer inherits the mistake. The model does not go back. It does not evaluate whether step one was actually correct before proceeding to step two. Once a chain is started, it runs in one direction. For a broad range of tasks, this does not matter much. Chain of Thought performs well on standard math problems, logical deductions, summarization tasks, and the kind of everyday reasoning that appears in most prompts. The single-path limitation is only a meaningful constraint when problems are genuinely ambiguous, when there are multiple plausible approaches worth exploring, or when the cost of an early error is high. Tree of Thoughts: Branching and Backtracking Tree of Thoughts extends the Chain of Thought idea by making the reasoning process non-linear. Rather than generating one chain of steps and following it to a conclusion, the model generates multiple possible next steps at each point, evaluates how promising each one looks, and selects the most viable path to pursue further. If a path leads to a dead end, the system backtracks and tries a different branch. The chess player analogy is useful here. A strong chess player does not just calculate the most obvious next move and commit to it. They consider multiple candidate moves, think through the implications of each, discard the ones that lead to bad positions, and pursue the one that looks most promising further. If deeper calculation reveals that the promising-looking move leads to a trap, they abandon it and revisit the alternatives. Tree of Thoughts applies this kind of deliberate search to language model reasoning. At each step, the model is asked to generate several distinct continuations rather than just one. It then evaluates those continuations, either by scoring them directly or by reasoning about which ones are more likely to lead to a correct answer. A search algorithm — the same kind used in classical computer science problems — guides which branches to explore and in what order. The result is a structured exploration of a space of possible reasoning paths rather than a single committed trajectory. This architecture allows the system to recover from mistakes in a way that Chain of Thought cannot. A branch that turns out to be wrong can be abandoned. A path that initially looked less promising can be revisited if the leading candidate fails. The model is not locked into a decision it made early in the process.
Access Denied
Access Denied You don’t have permission to access “http://zeenews.india.com/world/pace-yourself-vs-stop-frankenstein-nividi-ceo-jensen-huang-openai-chief-sam-altman-and-us-vp-jd-vance-clash-over-ai-safety-3071785.html” on this server. Reference #18.c4f43717.1789525657.504a6678 https://errors.edgesuite.net/18.c4f43717.1789525657.504a6678
Versioning and Tracking Scikit-LLM Experiments
In this article, you will learn how to build, track, compare, and register scikit-learn pipelines that integrate large language models using Scikit-LLM and MLflow. Topics we will cover include: How to configure Scikit-LLM and MLflow to support local large language model execution and experiment tracking. How to log multiple pipeline versions across different large language model backends and compare them using MLflow’s tracking API. How to promote the best-performing pipeline from a tracked experiment into MLflow’s Model Registry for deployment. Introduction Registering, versioning, and comparing scikit-learn-like pipelines that integrate large language models (LLMs) can be made easy with the aid of two cornerstone tools: the Scikit-LLM library and MLflow, an open-source framework for managing the end-to-end lifecycle of machine learning projects. This article demonstrates the steps to build, log, compare, and register scikit-learn pipelines revolving around LLMs using Scikit-LLM and MLflow. The code shown and described in detail below is designed with the primary purpose of ensuring model versioning and reproducibility across LLM backend updates — a frequent process in real settings that can quickly escalate. Setup and Initial Configurations If you haven’t done so before, or if you are running this code on a cloud-based notebook like Google Colab, the first step is to install the key libraries you will need: pip install “scikit-llm[gpt4all]” mlflow pip install “scikit-llm[gpt4all]” mlflow Make sure to use the extra option in brackets when installing scikit-llm to avoid compatibility issues. Now, we initialize the configuration of Scikit-LLM with dummy credentials that enable local gpt4all model execution. Meanwhile, the MLflow model registry —the key resource where models will be versioned— relies on a database backend, which is also configured in the code below. Moreover, we initialize an MLflow tracking experiment named “Scikit-LLM-Versioning”. Lastly, we define a small labeled dataset for zero-shot classification (more about this LLM-driven form of classification task here). import mlflow import mlflow.sklearn from sklearn.pipeline import Pipeline from skllm.config import SKLLMConfig from skllm.models.gpt.classification.zero_shot import ZeroShotGPTClassifier # 1. Dummy keys required by Scikit-LLM for local gpt4all execution SKLLMConfig.set_openai_key(“local-execution-key”) SKLLMConfig.set_openai_org(“local-execution-org”) # 2. Database backend required for the MLflow Model Registry mlflow.set_tracking_uri(“sqlite:///mlflow.db”) mlflow.set_experiment(“Scikit-LLM-Versioning”) # Sample dataset for zero-shot classification X_train = [ “The application crashed immediately.”, “Absolutely wonderful support team!”, “It works fine but is a bit slow.” ] y_train = [“bug”, “praise”, “feedback”] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import mlflow import mlflow.sklearn from sklearn.pipeline import Pipeline from skllm.config import SKLLMConfig from skllm.models.gpt.classification.zero_shot import ZeroShotGPTClassifier # 1. Dummy keys required by Scikit-LLM for local gpt4all execution SKLLMConfig.set_openai_key(“local-execution-key”) SKLLMConfig.set_openai_org(“local-execution-org”) # 2. Database backend required for the MLflow Model Registry mlflow.set_tracking_uri(“sqlite:///mlflow.db”) mlflow.set_experiment(“Scikit-LLM-Versioning”) # Sample dataset for zero-shot classification X_train = [ “The application crashed immediately.”, “Absolutely wonderful support team!”, “It works fine but is a bit slow.” ] y_train = [“bug”, “praise”, “feedback”] Logging the Baseline and Upgraded Pipelines This is where the real fun starts. We initialize a baseline pipeline that trains a zero-shot classification model using a lightweight pre-trained LLM. The with block that follows, named after the Orca Mini model selected, enables tracking of the LLM backend type and the model file string as environment parameters, thereby fostering reproducibility. A “cloudpickle” serialization format (a variant of the classic pickle, or .pkl for short, used in smaller machine learning models) is used to log the pipeline. Understanding this block is key to leveraging LLM versioning in MLflow for subsequent experiment tracking. Once execution completes, it outputs a unique MLflow run ID. LLM_V1 = “gpt4all::orca-mini-3k-71m-q4_0.gguf” pipeline_v1 = Pipeline([ (‘llm_classifier’, ZeroShotGPTClassifier(model=LLM_V1)) ]) with mlflow.start_run(run_name=”Baseline_Orca_Mini”) as run_v1: mlflow.log_param(“llm_backend”, “gpt4all”) mlflow.log_param(“llm_model_file”, LLM_V1) pipeline_v1.fit(X_train, y_train) # Override strict skops type checking with cloudpickle mlflow.sklearn.log_model( pipeline_v1, “model”, serialization_format=”cloudpickle” ) print(f”V1 Logged – Run ID: {run_v1.info.run_id}”) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 LLM_V1 = “gpt4all::orca-mini-3k-71m-q4_0.gguf” pipeline_v1 = Pipeline([ (‘llm_classifier’, ZeroShotGPTClassifier(model=LLM_V1)) ]) with mlflow.start_run(run_name=“Baseline_Orca_Mini”) as run_v1: mlflow.log_param(“llm_backend”, “gpt4all”) mlflow.log_param(“llm_model_file”, LLM_V1) pipeline_v1.fit(X_train, y_train) # Override strict skops type checking with cloudpickle mlflow.sklearn.log_model( pipeline_v1, “model”, serialization_format=“cloudpickle” ) print(f“V1 Logged – Run ID: {run_v1.info.run_id}”) Output excerpt: V1 Logged – Run ID: 0852aaec23364725b433f09973a3d911 V1 Logged – Run ID: 0852aaec23364725b433f09973a3d911 Next, let’s suppose we create a secondary, upgraded pipeline based on a heavier LLM to demonstrate MLflow’s model-swapping capabilities. Specifically, we now target “gpt4all::ggml-model-gpt4all-falcon-q4_0.bin”, which makes for a realistic backend upgrade. The code below isolates this new pipeline inside a separate MLflow run named “Upgraded_Falcon”. Everything else is done just as before: pipeline parameterization, model fitting, and logging —just in a distinct MLflow run, yielding a new unique ID. LLM_V2 = “gpt4all::ggml-model-gpt4all-falcon-q4_0.bin” pipeline_v2 = Pipeline([ (‘llm_classifier’, ZeroShotGPTClassifier(model=LLM_V2)) ]) with mlflow.start_run(run_name=”Upgraded_Falcon”) as run_v2: mlflow.log_param(“llm_backend”, “gpt4all”) mlflow.log_param(“llm_model_file”, LLM_V2) pipeline_v2.fit(X_train, y_train) # Override strict skops type checking with cloudpickle mlflow.sklearn.log_model( pipeline_v2, “model”, serialization_format=”cloudpickle” ) print(f”V2 Logged – Run ID: {run_v2.info.run_id}”) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 LLM_V2 = “gpt4all::ggml-model-gpt4all-falcon-q4_0.bin” pipeline_v2 = Pipeline([ (‘llm_classifier’, ZeroShotGPTClassifier(model=LLM_V2)) ]) with mlflow.start_run(run_name=“Upgraded_Falcon”) as run_v2: mlflow.log_param(“llm_backend”, “gpt4all”) mlflow.log_param(“llm_model_file”, LLM_V2) pipeline_v2.fit(X_train, y_train) # Override strict skops type checking with cloudpickle mlflow.sklearn.log_model( pipeline_v2, “model”, serialization_format=“cloudpickle” ) print(f“V2 Logged – Run ID: {run_v2.info.run_id}”) Output excerpt: V2 Logged – Run ID: ee892572d0a641f89201c33479b98746 V2 Logged – Run ID: ee892572d0a641f89201c33479b98746 Auditing, Comparing, and Registering Models Now that we have multiple logged pipeline versions, we invoke the MLflow search API to extract the full versioning experiment and display it as a pandas DataFrame. Note that key auditing columns have been separated for clarity: run ID, MLflow run name, local LLM parameter, and execution status. For a realistic touch, the results below (based on previous runs leading to the final code included in this article) show historical audit information from several executions — displaying not only MLflow tracking of FINISHED pipelines but also early FAILED attempts. experiment = mlflow.get_experiment_by_name(“Scikit-LLM-Versioning”) runs_df = mlflow.search_runs(experiment.experiment_id)