Skip to content

Support MLPerf training logging compliance for MaxText - #5118

Open
zxhe-sean wants to merge 1 commit into
mainfrom
mlperf_logging
Open

Support MLPerf training logging compliance for MaxText#5118
zxhe-sean wants to merge 1 commit into
mainfrom
mlperf_logging

Conversation

@zxhe-sean

Copy link
Copy Markdown
Collaborator

Description

This PR introduces end-to-end MLPerf training logging (mllog) compliance support into MaxText for MLPerf Training submissions.
When enabled (enable_mllog=true), MaxText logs all required initialization metadata, training step metrics (tracked_stats), evaluation intervals (block_start/block_stop, eval_start/eval_stop), evaluation accuracy milestones, and early stopping / run completion markers strictly following MLCommons Training compliance rules.

Key Changes

  1. MLPerf Logging Utilities (src/maxtext/utils/mllog_utils.py):
    • Rank 0 Process Isolation: Ensures all mllog calls are gated strictly to jax.process_index() == 0.
    • GCS Staging & Synchronization: Writes mllog records locally through mllog.config(filename=...) and periodically/synchronously syncs the staging log to base_output_directory on GCS using google-cloud-storage (client.bucket(...) avoiding bucket metadata permission requirements) with etils.epath fallback.
    • v6.0 Compliance Schema Support:
      • Emits submission_org, submission_platform, submission_division, submission_benchmark (deepseekv3_671b), and submission_status.
      • Logs AdamW optimizer keys with required naming: opt_adamw_beta_1, opt_adamw_beta_2, opt_adamw_epsilon, and opt_adamw_weight_decay.
      • Logs MoE auxiliary loss coefficient: moe_aux_loss_coeff (mapped from config.load_balance_loss_weight).
      • Logs learning rate schedule parameters: opt_base_learning_rate, opt_learning_rate_warmup_steps, opt_learning_rate_decay_steps, and opt_learning_rate_decay_schedule.
      • Accurately logs eval_samples, train_samples, init_checkpoint_step, and max_sequence_length.
    • Lifecycle Markers:
      • Emits init_start, cache_clear, init_stop, run_start, and initial block_start.
      • Emits tracked_stats on each completed training step with reduced_train_loss and train_step_time.
      • Hooks into evaluation with block_stop, eval_start, eval_accuracy, eval_stop, and conditional run_stop upon reaching target_eval_loss.
  2. Trainer & Metric Logger Integration (train.py, metric_logger.py):
    • train.py:
      • Calls mllog_utils.init_start(config) during initialization.
      • Triggers init_print, init_stop, run_start, and block_start before entering the step loop.
      • Logs tracked_stats and eval_start at evaluation checkpoints.
      • Emits run_stop(status='success') upon job completion.
    • metric_logger.py:
      • Calls mllog_utils.check_eval from finalize_eval_metrics with eval loss and step count.
      • Ensures mllog_utils.flush_and_sync(force=True) on writer close.
  3. Configuration Knobs (base.yml, types.py):
    • enable_mllog: bool = False: Flag to enable MLPerf logging (defaults to False for zero overhead during standard runs).
    • mllog_file: None | PathStr = "": Path or filename for exporting mllog entries (defaults to os.path.join(base_output_directory, run_name, "mllog.log")). Supports None | PathStr to handle empty CLI overrides (mllog_file="").
  4. Dependencies (requirements.txt, pre_train_github_deps.txt):
    • Upgraded mlperf-logging dependency to release 6.0.0-rc6.

BUGS: b/556312967

Notice 1: Once all tests pass, the "pull ready" label will automatically be assigned.
This label is used for administrative purposes. Please do not add it manually.

Notice 2: For external contributions, our settings currently require an approval from a MaxText maintainer to trigger CI tests.

Tests

Had a successful e2e run, the generated mllog file, also did a local RCP check with compliance_checker. It didn't pass the check as a few flag naming issue, and the mismatch of the mlperf_logging version.

I made the necessary change and verified locally for the missing flags.

Checklist

Before submitting this PR, please make sure (put X in square brackets):

  • I have performed a self-review of my code. For an optional AI review, add the gemini-review label.
  • I have necessary comments in my code, particularly in hard-to-understand areas.
  • I have run end-to-end tests tests and provided workload links above if applicable.
  • I have made or will make corresponding changes to the doc if needed, including adding new documentation pages to the relevant Table of Contents (toctree directive) as explained in our documentation.

@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@codecov

codecov Bot commented Sep 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.50000% with 133 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/maxtext/utils/mllog_utils.py 26.92% 121 Missing and 12 partials ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

🤖 Hi @RissyRan, I've received your request, and I'm working on it now! You can track my progress in the logs for more details.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

## 📋 Review Summary

This Pull Request introduces end-to-end MLPerf Training logging (mllog) compliance support in MaxText, aligning with MLCommons v6.0 schemas. It correctly structures rank-0 isolated logging and implements continuous synchronization of local compliance logs to GCS. The integration with the core training and evaluation loops is robust, well-sequenced, and introduces zero overhead when disabled.

🔍 General Feedback

  • Architecture and Integration: The integration with the core train.py lifecycle and MetricLogger is highly cohesive and does not disrupt standard execution paths.
  • Dependency Management: The update of the mlperf-logging dependency to the latest 6.0.0-rc6 tag is correctly aligned across package requirements and GitHub deps.
  • Robustness: The fallback synchronization using etils.epath if google-cloud-storage is absent ensures excellent runtime robustness.

Comment on lines +87 to +133
_last_sync_time = 0.0
_min_sync_interval = 5.0


def flush_and_sync(force=False):
"""Flushes mllog handlers and uploads the local staging file to GCS if needed."""
global _last_sync_time
if not _enabled or mllogger is None or jax.process_index() != 0:
return

if hasattr(mllogger, "logger") and mllogger.logger:
for handler in mllogger.logger.handlers:
try:
handler.flush()
except Exception: # pylint: disable=broad-exception-caught
pass

if _destination_path and _local_staging_file and _destination_path.startswith("gs://"):
import time # pylint: disable=import-outside-toplevel

now = time.time()
if force or (now - _last_sync_time >= _min_sync_interval):
_upload_file_to_gcs(_destination_path, _local_staging_file)
_last_sync_time = now


def _upload_file_to_gcs(dest_gcs: str, src_local: str):
"""Uploads a local file to GCS using google-cloud-storage client or etils.epath fallback."""
if not os.path.exists(src_local):
return
try:
from google.cloud import storage # pylint: disable=import-outside-toplevel

path_parts = dest_gcs.replace("gs://", "").split("/")
bucket_name = path_parts.pop(0)
blob_name = "/".join(path_parts)
client = storage.Client()
bucket = client.bucket(bucket_name)
blob = bucket.blob(blob_name)
blob.upload_from_filename(src_local)
except Exception: # pylint: disable=broad-exception-caught
try:
from etils import epath # pylint: disable=import-outside-toplevel

epath.Path(dest_gcs).write_bytes(epath.Path(src_local).read_bytes())
except Exception as e: # pylint: disable=broad-exception-caught
max_logging.log(f"Warning: Failed to sync mllog to GCS destination {dest_gcs}: {e}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High - Synchronous blocking network I/O in distributed training loop.

Currently, flush_and_sync is called on every step (via tracked_stats), and every 5 seconds it synchronously calls _upload_file_to_gcs to upload the local staging file to GCS. Because this is a blocking network call on Rank 0, it will stall/halt the entire distributed training cluster at the next collective sync.

Additionally, _upload_file_to_gcs creates a new GCS storage.Client instance on every sync, which incurs substantial connection and auth overhead.

Recommendation: Run the GCS upload in a background daemon thread to avoid blocking the training loop, and cache/reuse the GCS client.

Suggested change
_last_sync_time = 0.0
_min_sync_interval = 5.0
def flush_and_sync(force=False):
"""Flushes mllog handlers and uploads the local staging file to GCS if needed."""
global _last_sync_time
if not _enabled or mllogger is None or jax.process_index() != 0:
return
if hasattr(mllogger, "logger") and mllogger.logger:
for handler in mllogger.logger.handlers:
try:
handler.flush()
except Exception: # pylint: disable=broad-exception-caught
pass
if _destination_path and _local_staging_file and _destination_path.startswith("gs://"):
import time # pylint: disable=import-outside-toplevel
now = time.time()
if force or (now - _last_sync_time >= _min_sync_interval):
_upload_file_to_gcs(_destination_path, _local_staging_file)
_last_sync_time = now
def _upload_file_to_gcs(dest_gcs: str, src_local: str):
"""Uploads a local file to GCS using google-cloud-storage client or etils.epath fallback."""
if not os.path.exists(src_local):
return
try:
from google.cloud import storage # pylint: disable=import-outside-toplevel
path_parts = dest_gcs.replace("gs://", "").split("/")
bucket_name = path_parts.pop(0)
blob_name = "/".join(path_parts)
client = storage.Client()
bucket = client.bucket(bucket_name)
blob = bucket.blob(blob_name)
blob.upload_from_filename(src_local)
except Exception: # pylint: disable=broad-exception-caught
try:
from etils import epath # pylint: disable=import-outside-toplevel
epath.Path(dest_gcs).write_bytes(epath.Path(src_local).read_bytes())
except Exception as e: # pylint: disable=broad-exception-caught
max_logging.log(f"Warning: Failed to sync mllog to GCS destination {dest_gcs}: {e}")
_last_sync_time = 0.0
_min_sync_interval = 5.0
_gcs_client = None
_upload_thread = None
def flush_and_sync(force=False):
"""Flushes mllog handlers and uploads the local staging file to GCS if needed."""
global _last_sync_time, _upload_thread
if not _enabled or mllogger is None or jax.process_index() != 0:
return
if hasattr(mllogger, "logger") and mllogger.logger:
for handler in mllogger.logger.handlers:
try:
handler.flush()
except Exception: # pylint: disable=broad-exception-caught
pass
if _destination_path and _local_staging_file and _destination_path.startswith("gs://"):
import time # pylint: disable=import-outside-toplevel
now = time.time()
if force or (now - _last_sync_time >= _min_sync_interval):
if _upload_thread is not None and _upload_thread.is_alive():
if force:
_upload_thread.join() # If forced (e.g. final shutdown), block to complete
else:
return # Skip this interval to prevent overlap with a running upload
import threading # pylint: disable=import-outside-toplevel
_upload_thread = threading.Thread(
target=_upload_file_to_gcs,
args=(_destination_path, _local_staging_file),
daemon=True,
)
_upload_thread.start()
_last_sync_time = now
def _upload_file_to_gcs(dest_gcs: str, src_local: str):
"""Uploads a local file to GCS using google-cloud-storage client or etils.epath fallback."""
global _gcs_client
if not os.path.exists(src_local):
return
try:
from google.cloud import storage # pylint: disable=import-outside-toplevel
path_parts = dest_gcs.replace("gs://", "").split("/")
bucket_name = path_parts.pop(0)
blob_name = "/".join(path_parts)
if _gcs_client is None:
_gcs_client = storage.Client()
bucket = _gcs_client.bucket(bucket_name)
blob = bucket.blob(blob_name)
blob.upload_from_filename(src_local)
except Exception: # pylint: disable=broad-exception-caught
try:
from etils import epath # pylint: disable=import-outside-toplevel
epath.Path(dest_gcs).write_bytes(epath.Path(src_local).read_bytes())
except Exception as e: # pylint: disable=broad-exception-caught
max_logging.log(f"Warning: Failed to sync mllog to GCS destination {dest_gcs}: {e}")

limitations under the License.
"""

"""Utils for MLPerf submission compliance."""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Low - Missing unit tests for the newly added compliance logging utility.

The PR introduces a comprehensive logging utility in mllog_utils.py without any associated test coverage.

Recommendation: Add a unit test file (e.g. tests/unit/mllog_utils_test.py) to verify configurations, GCS staging, synchronization intervals, and compliance lifecycle logging methods using mock handlers.

Comment on lines +71 to +76
try:
with open(_local_staging_file, "w", encoding="utf8"):
pass
except Exception: # pylint: disable=broad-exception-caught
pass
mllog.config(filename=_local_staging_file)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium - Silent failure and subsequent crash if `/tmp` or staging log file is not writeable.

In setup_mllog, the try-except block checks if the file can be created but silently catches the exception and proceeds to call mllog.config(filename=_local_staging_file). If the file is indeed not writeable, mllog.config or subsequent logging calls will crash.

Recommendation: Safely disable logging and return early if staging file creation fails.

Suggested change
try:
with open(_local_staging_file, "w", encoding="utf8"):
pass
except Exception: # pylint: disable=broad-exception-caught
pass
mllog.config(filename=_local_staging_file)
try:
with open(_local_staging_file, "w", encoding="utf8"):
pass
mllog.config(filename=_local_staging_file)
except Exception as e: # pylint: disable=broad-exception-caught
max_logging.log(f"Warning: Staging file {_local_staging_file} is not writeable: {e}. MLPerf logging is disabled.")
_enabled = False
_is_configured = True
return

Comment on lines +181 to +194
mllogger.event(mllog.constants.SUBMISSION_ORG, "Google")
mllogger.event(mllog.constants.SUBMISSION_PLATFORM, "TPU-Ironwood")
mllogger.event(mllog.constants.SUBMISSION_STATUS, mllog.constants.CLOUD)
mllogger.event(mllog.constants.SUBMISSION_DIVISION, mllog.constants.CLOSED)

# Model specific
mllogger.event(mllog.constants.SUBMISSION_BENCHMARK, mllog.constants.DEEPSEEKV3_671B)
mllogger.event(mllog.constants.SEED, config.data_shuffle_seed)
mllogger.event(mllog.constants.MAX_STEPS, config.steps)
mllogger.event(mllog.constants.GLOBAL_BATCH_SIZE, config.global_batch_size_to_train_on)
mllogger.event(mllog.constants.GRADIENT_ACCUMULATION_STEPS, config.gradient_accumulation_steps)
mllogger.event(mllog.constants.MAX_SEQUENCE_LENGTH, config.max_target_length)
mllogger.event(mllog.constants.EVAL_SAMPLES, config.global_batch_size_to_eval_on * config.eval_steps)
mllogger.event(mllog.constants.TRAIN_SAMPLES, 1574207408)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium - Hardcoded compliance metadata limits benchmark portability.

The submission organization ("Google"), platform ("TPU-Ironwood"), benchmark ("DEEPSEEKV3_671B"), and train samples (1574207408) are hardcoded. This restricts the compliance utility to a single hardware and model configuration, which goes against MaxText's support for multiple platforms and models (e.g. Llama, Gemma, etc.).

Recommendation: Make these configurable via config with sensible default values.

Suggested change
mllogger.event(mllog.constants.SUBMISSION_ORG, "Google")
mllogger.event(mllog.constants.SUBMISSION_PLATFORM, "TPU-Ironwood")
mllogger.event(mllog.constants.SUBMISSION_STATUS, mllog.constants.CLOUD)
mllogger.event(mllog.constants.SUBMISSION_DIVISION, mllog.constants.CLOSED)
# Model specific
mllogger.event(mllog.constants.SUBMISSION_BENCHMARK, mllog.constants.DEEPSEEKV3_671B)
mllogger.event(mllog.constants.SEED, config.data_shuffle_seed)
mllogger.event(mllog.constants.MAX_STEPS, config.steps)
mllogger.event(mllog.constants.GLOBAL_BATCH_SIZE, config.global_batch_size_to_train_on)
mllogger.event(mllog.constants.GRADIENT_ACCUMULATION_STEPS, config.gradient_accumulation_steps)
mllogger.event(mllog.constants.MAX_SEQUENCE_LENGTH, config.max_target_length)
mllogger.event(mllog.constants.EVAL_SAMPLES, config.global_batch_size_to_eval_on * config.eval_steps)
mllogger.event(mllog.constants.TRAIN_SAMPLES, 1574207408)
submission_org = getattr(config, "submission_org", "Google")
submission_platform = getattr(config, "submission_platform", "TPU-Ironwood")
submission_benchmark = getattr(config, "submission_benchmark", mllog.constants.DEEPSEEKV3_671B)
train_samples = getattr(config, "mllog_train_samples", 1574207408)
mllogger.event(mllog.constants.SUBMISSION_ORG, submission_org)
mllogger.event(mllog.constants.SUBMISSION_PLATFORM, submission_platform)
mllogger.event(mllog.constants.SUBMISSION_STATUS, mllog.constants.CLOUD)
mllogger.event(mllog.constants.SUBMISSION_DIVISION, mllog.constants.CLOSED)
# Model specific
mllogger.event(mllog.constants.SUBMISSION_BENCHMARK, submission_benchmark)
mllogger.event(mllog.constants.SEED, config.data_shuffle_seed)
mllogger.event(mllog.constants.MAX_STEPS, config.steps)
mllogger.event(mllog.constants.GLOBAL_BATCH_SIZE, config.global_batch_size_to_train_on)
mllogger.event(mllog.constants.GRADIENT_ACCUMULATION_STEPS, config.gradient_accumulation_steps)
mllogger.event(mllog.constants.MAX_SEQUENCE_LENGTH, config.max_target_length)
mllogger.event(mllog.constants.EVAL_SAMPLES, config.global_batch_size_to_eval_on * config.eval_steps)
mllogger.event(mllog.constants.TRAIN_SAMPLES, train_samples)


self.write_metrics(self.cumulative_eval_metrics, train_step, metric_type="eval")
self._pending_eval_step_count = 0
mllog_utils.check_eval(self.config, train_step + 1, eval_loss, self.start_step)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check whether train_step has +1 before _finalize_eval_metrics.

mllog_utils.init_print(config, start_step)
mllog_utils.init_stop()
mllog_utils.run_start()
mllog_utils.block_start(config, start_step)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the block_start at the beginning. The block_stop is only in eval_start. If we do not have the eval. The block_stop will not be added.

@@ -0,0 +1,306 @@
"""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add unit tests for rcp logging?

)
mllogger.event(
mllog.constants.OPT_LR_DECAY_STEPS,
int(config.learning_rate_schedule_steps * (1 - config.warmup_steps_fraction) + 1),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why have +1? opt_learning_rate_decay_steps requires " v['value'] == 12000 - s['opt_learning_rate_warmup_steps'] ".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants