1. 项目概述当大模型实验撞上工程化瓶颈我们到底在解决什么问题你有没有经历过这样的场景凌晨两点盯着 Jupyter Notebook 里第 17 个model.fit()运行日志心里却没底——这个超参组合到底是不是最优昨天跑出来的 AUC 是 0.823今天换了个随机种子就掉到 0.791该信哪个更糟的是同事小张说他本地复现不了你的结果而你翻遍.ipynb文件也找不到他用的到底是transformers4.35.0还是4.36.2。这不是个别现象而是当前绝大多数 LLM 实验团队的真实日常。Scaling LLM Experimentation with SageMaker Pipelines and MLflow这个项目标题表面看是两个工具的组合技但内核直指一个被严重低估的工程痛点LLM 实验的不可复现性、不可追踪性与不可规模化。它不教你如何设计新架构也不讲怎么调出 SOTA 指标而是聚焦在“如何让每一次实验都像工厂流水线上的零件一样可登记、可回溯、可对比、可重跑”。核心关键词——SageMaker Pipelines定义可复用、可调度的端到端训练/评估/部署工作流、MLflow统一记录参数、指标、模型、代码快照与依赖环境——共同构成了一套面向生产级 LLM 研发的“实验操作系统”。它适合三类人一是刚从学术界转入工业界的算法工程师还在用git commit -m try lr5e-5管理实验二是带 3–5 人小团队的技术负责人正被成员间模型版本混乱、评估标准不一的问题拖慢迭代节奏三是 MLOps 工程师手头已有 SageMaker 基础设施但缺乏一套轻量、可嵌入现有流程的 LLM 实验治理方案。这不是一个“锦上添花”的优化项而是当你的实验从每周 2 次增长到每天 20 次时唯一能防止团队陷入混沌的技术护栏。2. 整体架构设计为什么必须是 Pipeline MLflow 的组合而不是单点工具2.1 单点工具的致命短板为什么只用 MLflow 或只用 Pipelines 都走不远很多团队初期会尝试“打补丁式”改进比如先上 MLflow把所有mlflow.log_param()和mlflow.log_metric()塞进 notebook或者直接用 SageMaker Pipelines 写一个训练 pipeline把数据预处理、模型训练、评估打包成一个 DAG。这两种做法短期内都能看到效果但很快就会触达天花板。我亲身经历过的三个典型崩塌时刻足以说明问题时刻一MLflow 单点记录的“断层”团队在 notebook 里用 MLflow 记录了 200 次实验参数、指标、模型 artifact 全都有。但某天要复现一个 3 个月前的 SOTA 结果时发现requirements.txt里只写了torch2.0实际运行时 pip 安装了2.1.2而那个最佳模型恰恰依赖2.0.1的某个 CUDA kernel 行为。MLflow 能存代码快照mlflow.log_artifact(train.py)但它不强制捕获运行时环境。你得手动conda env export environment.yml并再 log 一次而这个动作在快速迭代中极易遗漏。更麻烦的是MLflow 的run_id是随机字符串和 Git commit hash 没有绑定当你想查“commita1b2c3d对应哪次 run”时得靠人工在 UI 里翻找或写脚本关联效率极低。时刻二Pipelines 的“黑盒化”陷阱另一个团队用 Pipelines 构建了完美的训练 pipelinePreprocessStep → TrainStep → EvaluateStep → RegisterModelStep。每次pipeline.start()都生成一个清晰的 execution ID日志集中管理失败自动告警。听起来很美问题出在TrainStep内部。他们把整个训练逻辑封装在一个train.py脚本里而这个脚本里又硬编码了--learning_rate2e-5 --num_train_epochs3。当需要快速对比2e-5和3e-5时必须修改代码、提交新 commit、更新 pipeline definition、重新部署——整个过程耗时 15 分钟以上。Pipelines 擅长管理步骤间的依赖与调度但对步骤内部的超参敏感度与快速迭代支持极弱。它把实验变成了“发布流程”而不是“探索流程”。时刻三两者割裂导致的“双系统运维”最常见的情况是算法同学在本地用 MLflow 做快速试错记录一堆run_idMLOps 同学则用 Pipelines 在云端跑正式训练产出execution_id。两个系统完全独立没有 ID 映射没有元数据同步。当业务方问“线上模型 A 对应的是哪次实验”时答案往往是“我查下 Pipelines execution 日志……哦它调用了train.py版本 v1.2.3那对应的 MLflow run 应该是……大概在 3 月 15 号左右”——这种模糊匹配在模型审计或故障回溯时就是灾难。提示Pipeline 和 MLflow 不是“功能重叠”的竞品而是“职责互补”的搭档。Pipeline 是实验的骨架定义谁先谁后、谁依赖谁、谁触发谁MLflow 是实验的血肉记录每一次心跳、每一次呼吸、每一次微小的参数抖动。拆开用骨架没血肉是骷髅血肉没骨架是瘫痪。2.2 组合架构的核心设计哲学以“可重现的最小单元”为原子我们最终落地的架构并非简单地把 MLflow client 嵌入 Pipeline Step而是围绕一个核心理念重构每一次 Pipeline Execution必须对应且仅对应一次 MLflow Run每一次 Run 的生命周期必须由 Pipeline Step 的执行严格控制。这意味着Pipeline Step 是 MLflow Run 的“启动器”与“终结者”每个 Step如TrainStep在开始时调用mlflow.start_run()并传入run_nameftrain_{execution_id}在成功结束时调用mlflow.end_run()若 Step 失败则mlflow.end_run(statusFAILED)。这样execution_id 和 run_id 就天然绑定无需人工映射。超参注入不再是代码修改而是 Pipeline ParameterPipeline 定义中声明ParameterString(namelearning_rate, default_value2e-5)在TrainStep的processor或estimator启动命令中通过--learning-rate {learning_rate}注入。MLflow 在train.py中直接读取命令行参数并log_param(learning_rate, args.learning_rate)。改一个超参只需在 Pipeline 启动时传入新值无需改代码、无需新 commit。环境固化不是“尽力而为”而是“强制锁定”TrainStep使用的 SageMaker Processing Job 或 Training Job其容器镜像必须是预构建、带完整依赖的确定性镜像如my-llm-train:py39-torch201-cu118而非pytorch-training:2.0.1-cpu-py39这类可能随时间漂移的官方镜像。MLflow 的log_env功能在此只是补充真正的环境保障来自镜像本身。数据与模型版本化由 Pipeline Input/Output 承载Pipeline 的ProcessingInput指向 S3 上带版本号的数据集如s3://my-bucket/datasets/llm-finetune-v2.1/TrainingInput同理ModelStep的输出模型也存入s3://my-bucket/models/llm-finetune/{execution_id}/。MLflow 的log_model()只负责将模型 artifact 存入其 tracking server而真实可信的数据与模型来源永远是 Pipeline 定义中明确指定的 S3 URI。这解决了“MLflow model registry 里的模型到底用的是哪份数据训练的”这一根本问题。这套设计看似增加了前期配置成本但换来的是实验治理的“确定性”。当新同学入职他不需要去理解团队私有的命名规范或笔记习惯只要看 Pipeline Definition 和 MLflow UI就能在 5 分钟内搞清过去一周所有 LLM 微调实验的输入数据版本、超参组合、评估结果、以及如何一键重跑任意一次。3. 核心细节解析从零搭建一个可复用的 LLM 微调 Pipeline3.1 基础环境准备SageMaker Studio 与 MLflow Tracking Server 的协同配置在动手写 Pipeline 之前必须确保两个基础设施“握手成功”。这不是简单的“安装包”问题而是关于网络连通性、权限策略与默认行为对齐的实操细节。我见过太多团队卡在这一步反复调试数小时。首先MLflow Tracking Server 的部署方式选择。官方文档推荐用 EC2 或 ECS但对 SageMaker 用户最省心且安全的方式是在 SageMaker Studio Domain 内启动一个专用的mlflow-serverLifecycle Configuration (LC) 并绑定到System或User空间。具体操作如下创建一个 LC 脚本install-mlflow-server.sh#!/bin/bash set -e # 安装 mlflow 和依赖 pip install mlflow2.12.1 gunicorn21.2.0 # 创建 mlflow 数据目录使用 EFS确保跨 session 持久化 mkdir -p /home/sagemaker-user/mlflow-data # 创建启动脚本 cat /home/sagemaker-user/start-mlflow.sh EOF #!/bin/bash mlflow server \ --backend-store-uri file:///home/sagemaker-user/mlflow-data \ --default-artifact-root s3://my-mlflow-bucket/artifacts \ --host 0.0.0.0 \ --port 5000 \ --gunicorn-opts --timeout 120 --workers 4 EOF chmod x /home/sagemaker-user/start-mlflow.sh # 设置开机自启Studio Session 启动时 echo /home/sagemaker-user/start-mlflow.sh /etc/rc.local将此 LC 关联到 Studio Domain 的DefaultUserSettings并确保 Studio Execution Role 具有s3:GetObject,s3:PutObject权限针对my-mlflow-bucket以及ecr:GetAuthorizationToken如果后续要用自定义 ECR 镜像。注意不要用mlflow ui命令在 notebook 中启动它默认绑定127.0.0.1:5000Studio 内部无法访问。必须用--host 0.0.0.0并配合 Gunicorn。另外--backend-store-uri用file://是为了简化生产环境建议用 RDS PostgreSQL但file://对于中小团队已足够稳定。其次SageMaker Studio 内 MLflow Client 的初始化。在 notebook 中不能简单import mlflow; mlflow.set_tracking_uri(http://localhost:5000)。因为 Studio 的 notebook kernel 和 mlflow server 运行在不同容器中localhost指向 kernel 自身。正确做法是import mlflow from sagemaker.s3 import S3Downloader # 获取 mlflow server 在 Studio 内网的 DNS 名称关键 # Studio Domain 的 VPC 内mlflow server 会注册为 domain-id.studio.region.sagemaker.aws # 但更可靠的方式是在 LC 中写入一个环境变量 import os mlflow_uri os.getenv(MLFLOW_TRACKING_URI, http://mlflow-server:5000) mlflow.set_tracking_uri(mlflow_uri) # 验证连通性 try: mlflow.list_experiments() print(f✅ MLflow server connected at {mlflow_uri}) except Exception as e: print(f❌ Failed to connect to MLflow: {e}) # 此处可添加 fallback 逻辑如打印 debug 信息最后IAM 权限的“最小必要”原则。很多团队直接给 Studio Execution Role 加AdministratorAccess这是安全隐患。实际只需以下策略{ Version: 2012-10-17, Statement: [ { Effect: Allow, Action: [ s3:GetObject, s3:PutObject, s3:ListBucket ], Resource: [ arn:aws:s3:::my-mlflow-bucket/*, arn:aws:s3:::my-mlflow-bucket, arn:aws:s3:::my-dataset-bucket/*, arn:aws:s3:::my-model-bucket/* ] }, { Effect: Allow, Action: [ sagemaker:CreatePipeline, sagemaker:StartPipelineExecution, sagemaker:DescribePipelineExecution, sagemaker:ListPipelineExecutions ], Resource: * } ] }特别注意s3:ListBucket权限必须显式授予my-mlflow-bucket否则 MLflow server 无法列出 artifacts 目录UI 会报 403 错误。这个细节官方文档没写但踩过坑的人都懂。3.2 Pipeline Definition 编写从抽象概念到可执行代码一个健壮的 LLM 微调 Pipeline绝不是把train.py包裹一层就完事。它必须包含数据准备、模型加载、训练、评估、模型注册五个核心环节且每个环节都需考虑 LLM 场景的特殊性。下面以 LoRA 微调 Llama-2-7b 为例展示关键代码片段与设计理由。3.2.1 Pipeline 参数化让超参成为“第一公民”from sagemaker.workflow.parameters import ( ParameterInteger, ParameterString, ParameterFloat, ) # 定义 Pipeline 全局参数这些将成为启动时的输入 base_job_prefix ParameterString( nameBaseJobPrefix, default_valuellm-finetune ) # LLM 特定参数模型 ID、LoRA 配置、数据路径 model_id ParameterString( nameModelId, default_valuemeta-llama/Llama-2-7b-hf ) lora_r ParameterInteger( nameLoraR, default_value8 ) lora_alpha ParameterInteger( nameLoraAlpha, default_value16 ) lora_dropout ParameterFloat( nameLoraDropout, default_value0.1 ) # 数据参数S3 URI 必须带版本避免“最新数据”带来的不可控 train_data_uri ParameterString( nameTrainDataUri, default_values3://my-bucket/datasets/llm-finetune-v2.1/train/ ) eval_data_uri ParameterString( nameEvalDataUri, default_values3://my-bucket/datasets/llm-finetune-v2.1/eval/ ) # 训练参数batch size, learning rate, epochs —— 这些是算法同学最常调的 per_device_train_batch_size ParameterInteger( namePerDeviceTrainBatchSize, default_value4 ) learning_rate ParameterFloat( nameLearningRate, default_value2e-5 ) num_train_epochs ParameterInteger( nameNumTrainEpochs, default_value3 )实操心得为什么TrainDataUri和EvalDataUri是ParameterString而不是硬编码因为数据版本迭代比模型代码快得多。上周用v2.1这周可能就上线了修复标注错误的v2.2。把数据 URI 参数化意味着一次 Pipeline 更新就能覆盖所有数据版本的实验无需修改任何 Python 代码。这是工程化思维与科研思维的关键分水岭。3.2.2 Data Processing Step不只是格式转换更是数据质量门禁LLM 微调的数据质量直接决定模型上限。一个合格的ProcessingStep必须包含数据验证逻辑而不仅是pandas.read_json() → save_parquet()。from sagemaker.sklearn.processing import SKLearnProcessor from sagemaker.processing import ProcessingInput, ProcessingOutput # 使用自定义镜像内置了 fastparquet, datasets, transformers sklearn_processor SKLearnProcessor( framework_version1.0-1, rolerole, instance_typeml.m5.xlarge, instance_count1, base_job_namef{base_job_prefix}-preprocess, image_uri123456789012.dkr.ecr.us-east-1.amazonaws.com/my-llm-preprocess:latest ) # 输入原始 JSONL每行一个 {instruction: ..., input: ..., output: ...} # 输出Hugging Face Dataset 格式的 parquet支持 streaming, caching processing_step ProcessingStep( namePreprocessData, processorsklearn_processor, inputs[ ProcessingInput( sourcetrain_data_uri, destination/opt/ml/processing/input/train ), ProcessingInput( sourceeval_data_uri, destination/opt/ml/processing/input/eval ) ], outputs[ ProcessingOutput( output_nametrain_dataset, source/opt/ml/processing/output/train, destinationJoin( on/, values[ s3:/, bucket, prefix, datasets, processed, train, ExecutionVariables.PIPELINE_EXECUTION_ID ] ) ), ProcessingOutput( output_nameeval_dataset, source/opt/ml/processing/output/eval, destinationJoin( on/, values[ s3:/, bucket, prefix, datasets, processed, eval, ExecutionVariables.PIPELINE_EXECUTION_ID ] ) ) ], codepreprocess.py, # 此脚本在镜像中 job_arguments[ --train-input-dir, /opt/ml/processing/input/train, --eval-input-dir, /opt/ml/processing/input/eval, --output-dir, /opt/ml/processing/output, --max-seq-length, 1024, --min-output-length, 10, # 过滤掉太短的 output避免噪声 --deduplicate, True # 基于 instructioninput 的哈希去重 ] )preprocess.py的核心逻辑简略def validate_and_clean_dataset(dataset): LLM 数据清洗的黄金法则 # 规则1过滤掉 output 长度 10 的样本可能是标注错误或占位符 dataset dataset.filter(lambda x: len(x[output]) 10) # 规则2检查 instruction 是否为空或全是空格 dataset dataset.filter(lambda x: x[instruction].strip() ! ) # 规则3基于 content hash 去重避免同一问题重复出现 hashes [] for sample in dataset: h hashlib.md5((sample[instruction] sample[input]).encode()).hexdigest() if h not in hashes: hashes.append(h) else: continue # skip duplicate return dataset # 关键在处理结束时将数据统计写入 MLflow if __name__ __main__: mlflow.start_run(run_namefpreprocess_{os.getenv(PIPELINE_EXECUTION_ID)}) mlflow.log_param(input_train_uri, args.train_input_dir) mlflow.log_param(input_eval_uri, args.eval_input_dir) train_ds load_dataset(json, data_filesf{args.train_input_dir}/*.jsonl) train_ds validate_and_clean_dataset(train_ds) train_ds.to_parquet(f{args.output_dir}/train/dataset.parquet) # 记录清洗后的关键指标 mlflow.log_metric(train_samples_after_clean, len(train_ds)) mlflow.log_metric(eval_samples_after_clean, len(eval_ds)) mlflow.log_artifact(preprocess_report.json) # 生成详细报告 mlflow.end_run()注意ProcessingStep的输出 S3 URI 中嵌入了ExecutionVariables.PIPELINE_EXECUTION_ID这保证了每次 Pipeline 执行产生的数据都是隔离的不会被后续执行覆盖。这是实现“可重现”的基石。3.2.3 Training Step将 MLflow 深度融入训练循环这是整个 Pipeline 的心脏。我们使用 SageMaker Training Job而非 Processing Job因为它原生支持分布式训练和 GPU 优化。from sagemaker.pytorch import PyTorch # 使用预构建的、带完整依赖的 ECR 镜像 estimator PyTorch( entry_pointtrain.py, source_dirsrc, # 包含 train.py, requirements.txt, utils/ rolerole, instance_count2, # 2x g4dn.12xlarge for Llama-2-7b LoRA instance_typeml.g4dn.12xlarge, volume_size200, max_run3600 * 24, # 24 hours py_versionpy39, framework_version2.0.1, hyperparameters{ model_id: model_id, lora_r: lora_r, lora_alpha: lora_alpha, lora_dropout: lora_dropout, per_device_train_batch_size: per_device_train_batch_size, learning_rate: learning_rate, num_train_epochs: num_train_epochs, output_dir: /opt/ml/model, # SageMaker 默认模型输出路径 }, # 关键启用 MLflow 自动日志需在 train.py 中初始化 environment{ MLFLOW_TRACKING_URI: mlflow_uri, MLFLOW_EXPERIMENT_NAME: llm-finetune-experiment } ) training_step TrainingStep( nameTrainLLM, estimatorestimator, inputs{ train: TrainingInput( s3_dataprocessing_step.properties.ProcessingOutputConfig.Outputs[train_dataset].S3Output.S3Uri, content_typeapplication/x-parquet ), eval: TrainingInput( s3_dataprocessing_step.properties.ProcessingOutputConfig.Outputs[eval_dataset].S3Output.S3Uri, content_typeapplication/x-parquet ) } )train.py的核心结构重点看 MLflow 集成import mlflow import torch from transformers import AutoModelForCausalLM, TrainingArguments, Trainer from peft import LoraConfig, get_peft_model def main(): # 1. 解析 SageMaker 传入的超参 parser argparse.ArgumentParser() parser.add_argument(--model_id, typestr) parser.add_argument(--lora_r, typeint) parser.add_argument(--lora_alpha, typeint) parser.add_argument(--lora_dropout, typefloat) # ... 其他参数 args parser.parse_args() # 2. 初始化 MLflow Run —— 必须在 Trainer 创建前 # SageMaker 会自动设置 JOB_NAME 和 TRAINING_JOB_ARN run_name ftrain-{os.getenv(JOB_NAME, unknown)} mlflow.start_run(run_namerun_name) # 3. 记录所有超参包括 SageMaker 自动注入的 for k, v in vars(args).items(): mlflow.log_param(k, v) mlflow.log_param(sagemaker_job_arn, os.getenv(TRAINING_JOB_ARN)) # 4. 加载基础模型和 LoRA 配置 model AutoModelForCausalLM.from_pretrained(args.model_id) peft_config LoraConfig( rargs.lora_r, lora_alphaargs.lora_alpha, lora_dropoutargs.lora_dropout, target_modules[q_proj, v_proj], # Llama-2 的关键模块 ) model get_peft_model(model, peft_config) # 5. 创建 Trainer并启用 MLflow 自动日志关键 training_args TrainingArguments( output_dirargs.output_dir, per_device_train_batch_sizeargs.per_device_train_batch_size, learning_rateargs.learning_rate, num_train_epochsargs.num_train_epochs, # 启用 MLflow 回调 report_tomlflow, run_namerun_name, # 其他必要参数... ) trainer Trainer( modelmodel, argstraining_args, train_datasettrain_dataset, eval_dataseteval_dataset, # MLflow 回调会自动记录 metrics, params, model ) # 6. 开始训练 trainer.train() # 7. 训练结束后手动记录一些关键指标 eval_results trainer.evaluate() for k, v in eval_results.items(): mlflow.log_metric(feval_{k}, v) # 8. 保存 LoRA adapter不是整个模型 model.save_pretrained(f{args.output_dir}/lora-adapter) mlflow.log_artifact(f{args.output_dir}/lora-adapter) # 9. 结束 Run mlflow.end_run() if __name__ __main__: main()实操心得report_tomlflow是 Hugging Face Transformers 的原生集成它会自动记录loss,learning_rate,epoch等指标。但它不会记录你自定义的评估指标如 custom_bleu_score或模型结构信息。所以必须在trainer.evaluate()后手动mlflow.log_metric()。另外“保存 LoRA adapter” 而非整个模型是为了体积和部署效率——一个完整的 Llama-2-7b 是 13GB而 LoRA adapter 通常只有 20MB。4. 实操过程详解一次完整的 Pipeline 执行与结果分析4.1 启动 Pipeline从参数配置到 execution ID 生成Pipeline 的启动是整个实验流程的“发令枪”。它不像运行一个脚本那么简单而是一次对实验意图的正式声明。我们以一次典型的 A/B 测试为例对比 LoRA rankr8和r16对模型困惑度Perplexity的影响。首先在 SageMaker Studio notebook 中实例化 Pipeline 并配置参数from sagemaker.workflow.pipeline import Pipeline # 创建 Pipeline 实例 llm_pipeline Pipeline( nameLLM-Finetune-Pipeline, parameters[ base_job_prefix, model_id, lora_r, lora_alpha, lora_dropout, train_data_uri, eval_data_uri, per_device_train_batch_size, learning_rate, num_train_epochs ], steps[processing_step, training_step, evaluation_step, register_step], sagemaker_sessionsagemaker_session ) # 启动第一次执行r8 execution_1 llm_pipeline.start( parameters{ BaseJobPrefix: ab-test-r8, ModelId: meta-llama/Llama-2-7b-hf, LoraR: 8, # 关键差异点 LoraAlpha: 16, LoraDropout: 0.1, TrainDataUri: s3://my-bucket/datasets/llm-finetune-v2.1/train/, EvalDataUri: s3://my-bucket/datasets/llm-finetune-v2.1/eval/, PerDeviceTrainBatchSize: 4, LearningRate: 2e-5, NumTrainEpochs: 3 } ) print(f✅ Pipeline Execution 1 started: {execution_1.arn}) print(f Execution ID: {execution_1.execution_id}) # 输出类似llm-finetune-pipeline-123456789012-us-east-1-abc123def456几秒钟后SageMaker 控制台会显示一个全新的 Pipeline Execution状态为Executing。此时后台发生了什么SageMaker 创建 Execution Context为本次执行分配唯一的execution_id并初始化所有Parameter的值。ProcessingStep 启动SageMaker 启动一个ml.m5.xlarge实例拉取my-llm-preprocess:latest镜像挂载输入 S3 数据执行preprocess.py。该脚本内部调用mlflow.start_run(run_namefpreprocess_{execution_id})在 MLflow UI 中创建一个名为preprocess-llm-finetune-pipeline-123456789012-us-east-1-abc123def456的 Run。TrainingStep 启动Processing 成功后SageMaker 启动 2 个ml.g4dn.12xlarge实例拉取训练镜像挂载上一步输出的 processed data S3 URI执行train.py。train.py中mlflow.start_run(run_nameftrain-{os.getenv(JOB_NAME)})创建第二个 Run名称为train-llm-finetune-pipeline-123456789012-us-east-1-abc123def456-00001SageMaker 自动生成 JOB_NAME。Execution ID 与 Run ID 的映射关系虽然 Run 名称不同但它们的tags中都包含了sagemaker:execution-id: llm-finetune-pipeline-123456789012-us-east-1-abc123def456。这是我们在preprocess.py和train.py中主动添加的# 在 mlflow.start_run() 后 mlflow.set_tag(sagemaker:execution-id, os.getenv(PIPELINE_EXECUTION_ID, unknown))这个 tag 是后续在 MLflow UI 中按 Execution ID 过滤所有相关 Runs 的唯一依据。提示不要依赖 Run Name 的字符串匹配因为JOB_NAME可能包含-00001后缀而PIPELINE_EXECUTION_ID是纯净的。tag 是结构化、可编程查询的Name 是给人看的。4.2 结果分析如何在 MLflow UI 中进行多维对比当 Pipeline 执行完毕假设成功打开 MLflow UI你会看到至少两个 Runspreprocess 和 train它们都带有相同的sagemaker:execution-idtag。点击左侧Experiments进入llm-finetune-experiment然后使用右上角的搜索框tags.sagemaker:execution-id llm-finetune-pipeline-123456789012-us-east-1-abc123def456这会筛选出本次执行的所有 Runs。现在进行关键的 A/B 分析4.2.1 超参对比确认实验变量唯一性在 Runs 列表页勾选preprocess-xxx和train-xxx两个 Run点击Compare。在Parameters标签页你会看到Parameterpreprocess-xxxtrain-xxxlora_r—8model_id—meta-llama/Llama-2-7b-hfinput_train_uris3://.../v2.1/train/—这立刻证明lora_r8是本次实验中唯一被改变的变量其他所有参数包括数据 URI都保持一致。这是科学实验的铁律。4.2.2 指标对比量化 rank 对性能的影响切换到Metrics标签页重点关注eval_perplexity评估集困惑度越低越好Metricpreprocess-xxxtrain-xxxeval_perplexity—12.34train_loss—1.87eval_samples_after_clean12500—eval_perplexity12.34是核心结果。但别急着下结论再启动第二次 Pipeline Execution将LoraR改为16execution_2 llm_pipeline.start( parameters{ BaseJobPrefix: ab-test-r16, LoraR: 16, # 唯一变化 # ... 其他参数完全相同 } )等execution_2完成后再次在 MLflow UI 中搜索tags.sagemaker:execution-id llm-finetune-pipeline-123456789012-us-east-1-xyz789uvw012得到eval_perplexity11.92。对比Execution IDlora_reval_perplexityChangeabc123def456812.34—xyz789uvw0121611.92-0.42一个看似微小的-0.42在 LLM 领域可能意味着生成质量的显著提升。更重要的是这个对比是在完全相同的数据、相同的代码、相同的硬件、相同的随机种子我们在TrainingArguments中设置了seed42下完成的。这就是 Pipeline MLflow 组合带来的确定性。4.2.