Add Decoding Verification for the Checkpoint Validation Pipeline - #4728
Add Decoding Verification for the Checkpoint Validation Pipeline#4728olufiyin19 wants to merge 7 commits into
Conversation
…dation for the Checkpoint Validation Agent
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
🤖 Hi @entrpn, I've received your request, and I'm working on it now! You can track my progress in the logs for more details. |
There was a problem hiding this comment.
This Pull Request successfully introduces a comprehensive, automated autoregressive decoding and generative validation pipeline via decode_validator.py, completing the fourth stage of the post-checkpoint conversion validation suite. The design of standardizing reporting to GCS in JSON format and streaming logs is a valuable addition to enable automated fail-fast diagnostics under Airflow orchestration.
🔍 General Feedback
- Excellent Architectural Consistency: The scripts align nicely with the transition to pure NNX modules while maintaining backwards compatibility where appropriate.
- Robust Exception Handling Pattern: While the standard execution flow is clean, the error handling around initial file reading and subprocess timeouts should be reinforced to ensure that JSON reports are guaranteed to write and upload even under catastrophic failures.
- Process Cleanup Discipline: It is crucial to clean up global monkeypatches in Python
finallyblocks when scripts are executed in-process, to prevent polluting the shared runtime process space and introducing flaky test behaviors.
| # applying a monkeypatch to maxtext's model_creation_utils because it has a bug where | ||
| # it cannot resolve SequenceKey (list indices) to string keys in Linen checkpoints. | ||
|
|
||
| source = inspect.getsource(model_creation_utils._fix_restore_args_for_shape_mismatch) # pylint: disable=protected-access |
There was a problem hiding this comment.
| # applying a monkeypatch to maxtext's model_creation_utils because it has a bug where | |
| # it cannot resolve SequenceKey (list indices) to string keys in Linen checkpoints. | |
| source = inspect.getsource(model_creation_utils._fix_restore_args_for_shape_mismatch) # pylint: disable=protected-access | |
| # applying a monkeypatch to maxtext's model_creation_utils because it has a bug where | |
| # it cannot resolve SequenceKey (list indices) to string keys in Linen checkpoints. | |
| _original_fix_restore = model_creation_utils._fix_restore_args_for_shape_mismatch | |
| source = inspect.getsource(_original_fix_restore) # pylint: disable=protected-access |
| if _orig_array_delete is not None: | ||
| jax.Array.delete = _orig_array_delete | ||
| transformers.AutoTokenizer.from_pretrained = _orig_from_pretrained | ||
| sys.stdout = old_stdout | ||
| sys.stderr = old_stderr | ||
| os.chdir(old_cwd) | ||
|
|
There was a problem hiding this comment.
| if _orig_array_delete is not None: | |
| jax.Array.delete = _orig_array_delete | |
| transformers.AutoTokenizer.from_pretrained = _orig_from_pretrained | |
| sys.stdout = old_stdout | |
| sys.stderr = old_stderr | |
| os.chdir(old_cwd) | |
| finally: | |
| ocp.Checkpointer.restore = _original_restore | |
| model_creation_utils._fix_restore_args_for_shape_mismatch = _original_fix_restore | |
| if _orig_array_delete is not None: | |
| jax.Array.delete = _orig_array_delete | |
| transformers.AutoTokenizer.from_pretrained = _orig_from_pretrained | |
| sys.stdout = old_stdout | |
| sys.stderr = old_stderr | |
| os.chdir(old_cwd) |
| target_lookup = r" def _lookup_stored_meta\(path\):[\s\S]*?(?=\n\s*mismatched_paths_sharded = \[\])" | ||
| patched_source = re.sub(target_lookup, new_lookup, source) | ||
|
|
||
| env = dict(model_creation_utils.__dict__) | ||
| exec(patched_source, env) # pylint: disable=exec-used | ||
| model_creation_utils._fix_restore_args_for_shape_mismatch = env[ # pylint: disable=protected-access |
There was a problem hiding this comment.
| # run subprocess (from the top level repo directory) | ||
| result = subprocess.run(command, text=True, capture_output=True, check=False, cwd=repo_root) | ||
| if result.stdout: | ||
| logger.info("=== Subprocess Stdout ===") | ||
| logger.info(result.stdout) | ||
|
|
||
| # generate report | ||
| report = { | ||
| "run_name": run_name, | ||
| "model": internal_model_name, | ||
| "status": "SUCCESS" if result.returncode == 0 else "FAILED", | ||
| "success": result.returncode == 0, # if returncode is 0, command worked | ||
| "stdout": result.stdout, # store standard output (contains generated text like "Input ... -> ...") | ||
| "stderr": (result.stderr if result.returncode != 0 else "Success"), # store error message if there's a failure | ||
| "checkpoint_used": checkpoint_path, | ||
| } |
There was a problem hiding this comment.
| # run subprocess (from the top level repo directory) | |
| result = subprocess.run(command, text=True, capture_output=True, check=False, cwd=repo_root) | |
| if result.stdout: | |
| logger.info("=== Subprocess Stdout ===") | |
| logger.info(result.stdout) | |
| # generate report | |
| report = { | |
| "run_name": run_name, | |
| "model": internal_model_name, | |
| "status": "SUCCESS" if result.returncode == 0 else "FAILED", | |
| "success": result.returncode == 0, # if returncode is 0, command worked | |
| "stdout": result.stdout, # store standard output (contains generated text like "Input ... -> ...") | |
| "stderr": (result.stderr if result.returncode != 0 else "Success"), # store error message if there's a failure | |
| "checkpoint_used": checkpoint_path, | |
| } | |
| # run subprocess with real-time streaming (from the top level repo directory) | |
| logger.info("=== Subprocess Stdout ===") | |
| try: | |
| with subprocess.Popen( | |
| command, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.PIPE, | |
| text=True, | |
| bufsize=1, | |
| cwd=repo_root, | |
| ) as proc: | |
| stdout_lines = [] | |
| for line in proc.stdout: | |
| logger.info(line.rstrip()) | |
| stdout_lines.append(line) | |
| stderr_str = proc.stderr.read() | |
| proc.wait(timeout=1800) # 30 minutes timeout | |
| returncode = proc.returncode | |
| stdout_str = "".join(stdout_lines) | |
| except subprocess.TimeoutExpired as e: | |
| proc.kill() | |
| stdout_str, stderr_str = proc.communicate() | |
| returncode = -1 | |
| logger.error("Subprocess decode.py timed out after 30 minutes!") | |
| except Exception as e: | |
| returncode = -1 | |
| stdout_str = "" | |
| stderr_str = str(e) | |
| # generate report | |
| report = { | |
| "run_name": run_name, | |
| "model": internal_model_name, | |
| "status": "SUCCESS" if returncode == 0 else "FAILED", | |
| "success": returncode == 0, # if returncode is 0, command worked | |
| "stdout": stdout_str, # store standard output (contains generated text like "Input ... -> ...") | |
| "stderr": (stderr_str if returncode != 0 else "Success"), # store error message if there's a failure | |
| "checkpoint_used": checkpoint_path, | |
| } |
| """Computes cosine similarity between two layer activation arrays.""" | ||
| try: | ||
| a = np.asarray(arr1, dtype=np.float64).flatten() | ||
| b = np.asarray(arr2, dtype=np.float64).flatten() | ||
| min_len = min(a.size, b.size) | ||
| if min_len == 0: | ||
| return 0.0 | ||
| a = a[:min_len] | ||
| b = b[:min_len] | ||
| norm_a = np.linalg.norm(a) | ||
| norm_b = np.linalg.norm(b) | ||
| if norm_a == 0 or norm_b == 0: | ||
| return 0.0 | ||
| return float(np.dot(a, b) / (norm_a * norm_b)) | ||
| except Exception: # pylint: disable=broad-exception-caught | ||
| return 0.0 | ||
|
|
||
|
|
There was a problem hiding this comment.
| """Computes cosine similarity between two layer activation arrays.""" | |
| try: | |
| a = np.asarray(arr1, dtype=np.float64).flatten() | |
| b = np.asarray(arr2, dtype=np.float64).flatten() | |
| min_len = min(a.size, b.size) | |
| if min_len == 0: | |
| return 0.0 | |
| a = a[:min_len] | |
| b = b[:min_len] | |
| norm_a = np.linalg.norm(a) | |
| norm_b = np.linalg.norm(b) | |
| if norm_a == 0 or norm_b == 0: | |
| return 0.0 | |
| return float(np.dot(a, b) / (norm_a * norm_b)) | |
| except Exception: # pylint: disable=broad-exception-caught | |
| return 0.0 | |
| def compute_cosine_similarity(arr1: Any, arr2: Any) -> float: | |
| """Computes cosine similarity between two layer activation arrays.""" | |
| try: | |
| a = np.asarray(arr1, dtype=np.float64) | |
| b = np.asarray(arr2, dtype=np.float64) | |
| if a.shape != b.shape: | |
| logger.warning("Shape mismatch in cosine similarity: %s vs %s", a.shape, b.shape) | |
| return 0.0 | |
| a = a.flatten() | |
| b = b.flatten() | |
| if a.size == 0: | |
| return 0.0 | |
| norm_a = np.linalg.norm(a) | |
| norm_b = np.linalg.norm(b) | |
| if norm_a == 0 or norm_b == 0: | |
| return 0.0 | |
| return float(np.dot(a, b) / (norm_a * norm_b)) | |
| except Exception: # pylint: disable=broad-exception-caught | |
| return 0.0 |
| if hf_arr is not None and mt_arr is not None: | ||
| cos_sim = compute_cosine_similarity(hf_arr, mt_arr) | ||
| row["cosine_similarity"] = cos_sim | ||
| if cos_sim < 0.98 and first_divergence_layer is None: | ||
| first_divergence_layer = idx | ||
| else: | ||
| row["cosine_similarity"] = None |
There was a problem hiding this comment.
| if hf_arr is not None and mt_arr is not None: | |
| cos_sim = compute_cosine_similarity(hf_arr, mt_arr) | |
| row["cosine_similarity"] = cos_sim | |
| if cos_sim < 0.98 and first_divergence_layer is None: | |
| first_divergence_layer = idx | |
| else: | |
| row["cosine_similarity"] = None | |
| if hf_arr is not None and mt_arr is not None: | |
| cos_sim = compute_cosine_similarity(hf_arr, mt_arr) | |
| row["cosine_similarity"] = cos_sim | |
| import math | |
| is_diverged = (cos_sim < 0.98 or math.isnan(cos_sim)) if cos_sim is not None else False | |
| if is_diverged and first_divergence_layer is None: | |
| first_divergence_layer = idx | |
| else: | |
| row["cosine_similarity"] = None |
| ideal_shapes = load_shapes(args.ideal_shapes_path) | ||
| actual_shapes = load_shapes(args.actual_shapes_path) | ||
|
|
||
| _has_mismatch, _mismatched_layers = check_mismatches(ideal_shapes, actual_shapes) | ||
|
|
||
| report = { | ||
| "task": "checkpoint_shape_validation", | ||
| "timestamp": time.time(), | ||
| "status": "FAILURE" if _has_mismatch else "SUCCESS", | ||
| "mismatches_found": _has_mismatch, | ||
| "mismatched_layers": _mismatched_layers, | ||
| } |
There was a problem hiding this comment.
| ideal_shapes = load_shapes(args.ideal_shapes_path) | |
| actual_shapes = load_shapes(args.actual_shapes_path) | |
| _has_mismatch, _mismatched_layers = check_mismatches(ideal_shapes, actual_shapes) | |
| report = { | |
| "task": "checkpoint_shape_validation", | |
| "timestamp": time.time(), | |
| "status": "FAILURE" if _has_mismatch else "SUCCESS", | |
| "mismatches_found": _has_mismatch, | |
| "mismatched_layers": _mismatched_layers, | |
| } | |
| try: | |
| ideal_shapes = load_shapes(args.ideal_shapes_path) | |
| actual_shapes = load_shapes(args.actual_shapes_path) | |
| _has_mismatch, _mismatched_layers = check_mismatches(ideal_shapes, actual_shapes) | |
| status = "FAILURE" if _has_mismatch else "SUCCESS" | |
| except FileNotFoundError as e: | |
| logger.error("ERROR: Shape file not found: %s", e) | |
| _has_mismatch = True | |
| _mismatched_layers = [] | |
| status = "FAILURE" | |
| report = { | |
| "task": "checkpoint_shape_validation", | |
| "timestamp": time.time(), | |
| "status": status, | |
| "mismatches_found": _has_mismatch, | |
| "mismatched_layers": _mismatched_layers, | |
| } |
Description
This PR introduces
decode_validator.py, adding automated autoregressive decoding and generative inference validation to the automated post checkpoint conversion validation pipeline. This is the fourth phase in the stacked checkpoint validation series (building on PR #4462, PR #4726, and #4727).While static shape checks and single-step forward-pass logit verification confirm that converted checkpoints (e.g. Gemma, Llama, Deepseek, Mistral, Qwen) align with expected network geometries and logit distributions, they do not validate multi-step autoregressive generation. Converted checkpoints can pass single-step forward checks yet still fail during text generation due to KV-cache allocation errors, RoPE/positional embedding misconfigurations, attention mask incompatibilities, or tokenizer decode incompatibilities.
This PR standardizes generative validation by utilizing MaxText's autoregressive decoding pipeline (
src/maxtext/inference/decode.py), streaming subprocess stdout/stderr directly into application logs for remote observability, and publishing schema-compliant JSON reports (report_{run_name}.json) to Google Cloud Storage for Airflow fail-fast orchestration.Key Implementation Details
validate_checkpoint)src/maxtext/inference/decode.pythat executes cleanly from the repository root directory (repo_root).tokenizer_pathandscan_layers), raising immediate descriptive exceptions if required checkpoint metadata is omitted from Airflow or CLI overrides.=== Subprocess Stdout ===) directly intoabsl.logging/ MaxText logger so that generated prompt-response sequences (Input ... -> ...), token generation latency, and throughput metrics are visible in real time within Airflow and CI task logs.SUCCESSvsFAILED), and error diagnostics into a structured JSON report (report_{run_name}.json)../reports/report_{run_name}.json) and uploads them to Google Cloud Storage (report_gcs_dir), enabling Airflow fail-fast pipelines and the automated Fixer Agent Sidecar to inspect decoding fidelity and trigger self-healing loops upon failure.Tests
Verified autoregressive decoding execution, real-time subprocess logging, and JSON report generation across 4 distinct models. The script correctly enforces required overrides, parses custom GCS checkpoint/tokenizer paths, and uploads structured reports to GCS upon completion.
1. Qwen3-14b
Airflow Trigger JSON:
{ "checkpoint_gcs_path": "gs://maxtext-model-checkpoints/qwen3-14b/scanned/2026-04-12/0/items", "hf_config_url": "https://huggingface.co/Qwen/Qwen3-14B/raw/main/config.json", "hf_model_path": "Qwen/Qwen3-14B", "hf_ref_code_url": "https://raw.githubusercontent.com/huggingface/transformers/main/src/transformers/models/qwen3/modeling_qwen3.py", "hf_token": "***", "maxtext_branch": "feature/checkpoint-validation-clean", "maxtext_commit_hash": null, "maxtext_model_name": "qwen3-14b", "maxtext_overrides": { "attention": "dot_product", "debug_tensors": true, "max_prefill_predict_length": 16, "max_target_length": 128, "per_device_batch_size": 4, "prompt": "I love to ", "scan_layers": true, "tokenizer_path": "Qwen/Qwen3-14B", "tokenizer_type": "huggingface" }, "report_gcs_dir": "gs://maxtext-validation-agent-reports/", "run_name": "master-qwen3-14b-full-run" }Decoding Validation Task Execution Log:
https://paste.googleplex.com/5045773933871104
2. Mistral-7b
Airflow Trigger JSON:
{ "checkpoint_gcs_path": "gs://maxtext-model-checkpoints/mistral-7b/2025-01-23-19-04/unscanned/checkpoints/0/items", "hf_config_url": "https://huggingface.co/mistralai/Mistral-7B-v0.1/raw/main/config.json", "hf_model_path": "mistralai/Mistral-7B-v0.1", "hf_ref_code_url": "https://raw.githubusercontent.com/huggingface/transformers/main/src/transformers/models/mistral/modeling_mistral.py", "hf_token": "***", "maxtext_branch": "feature/checkpoint-validation-clean", "maxtext_commit_hash": null, "maxtext_model_name": "mistral-7b", "maxtext_overrides": { "attention": "dot_product", "debug_tensors": true, "dtype": "bfloat16", "max_prefill_predict_length": 16, "max_target_length": 128, "per_device_batch_size": 4, "prompt": "I love to ", "scan_layers": false, "tokenizer_path": "mistralai/Mistral-7B-v0.1", "tokenizer_type": "huggingface" }, "report_gcs_dir": "gs://maxtext-validation-agent-reports/", "run_name": "master-mistral-7b-full-run" }Decoding Validation Task Execution Log:
https://paste.googleplex.com/4642963094372352
3. Qwen3-8b
Airflow Trigger JSON:
{ "checkpoint_gcs_path": "gs://maxtext-model-checkpoints/qwen3-8b/unscanned/0/items", "hf_config_url": "https://huggingface.co/Qwen/Qwen3-8B/raw/main/config.json", "hf_model_path": "Qwen/Qwen3-8B", "hf_ref_code_url": "https://raw.githubusercontent.com/huggingface/transformers/main/src/transformers/models/qwen3/modeling_qwen3.py", "hf_token": "***", "maxtext_branch": "feature/checkpoint-validation-clean", "maxtext_commit_hash": null, "maxtext_model_name": "qwen3-8b", "maxtext_overrides": { "attention": "dot_product", "debug_tensors": true, "max_prefill_predict_length": 16, "max_target_length": 128, "per_device_batch_size": 4, "prompt": "I love to ", "scan_layers": false, "tokenizer_path": "Qwen/Qwen3-8B", "tokenizer_type": "huggingface" }, "report_gcs_dir": "gs://maxtext-validation-agent-reports/", "run_name": "master-qwen3-8b-full-run" }Decoding Validation Task Execution Log:
https://paste.googleplex.com/6236433022058496
4. Gemma3-4b
Airflow Trigger JSON:
{ "checkpoint_gcs_path": "gs://maxtext-model-checkpoints/gemma3-4b/2025-03-18-19-03/unscanned/checkpoints/0/items", "hf_config_url": "https://huggingface.co/google/gemma-3-4b-it/raw/main/config.json", "hf_model_path": "google/gemma-3-4b-it", "hf_ref_code_url": "https://raw.githubusercontent.com/huggingface/transformers/main/src/transformers/models/gemma3/modeling_gemma3.py", "hf_token": "***", "maxtext_branch": "feature/checkpoint-validation-clean", "maxtext_commit_hash": null, "maxtext_model_name": "gemma3-4b", "maxtext_overrides": { "attention": "dot_product", "debug_tensors": true, "max_prefill_predict_length": 16, "max_target_length": 128, "per_device_batch_size": 4, "prompt": "I love to ", "scan_layers": false, "tokenizer_path": "google/gemma-3-4b-it", "tokenizer_type": "huggingface" }, "report_gcs_dir": "gs://maxtext-validation-agent-reports/", "run_name": "master-gemma3-4b-full-run" }Decoding Validation Task Execution Log:
https://paste.googleplex.com/4656868940185600
Stack
4th PR in stack. Depends on the preceding upstream PR: #4727
Checklist
Before submitting this PR, please make sure (put X in square brackets):
gemini-reviewlabel.