lvkaokao commited on
Commit
abf5bac
Β·
1 Parent(s): 78f808c

add model-free.

Browse files

Signed-off-by: lkk12014402 <kaokao.lv@intel.com>

app.py CHANGED
@@ -86,7 +86,7 @@ from src.leaderboard.read_auto_pipeline_results import (
86
  )
87
  from src.populate import get_auto_pipeline_results_df, get_evaluation_queue_df
88
  from src.scripts.update_all_request_files import update_dynamic_files
89
- from src.submission.check_validity import GPU_COUNT_CHOICES, GPU_HARDWARE_CHOICES, CONTAINER_DISK_SIZE_CHOICES
90
  from src.tools.plots import (
91
  compute_metrics_summary,
92
  create_scheme_accuracy_chart,
@@ -119,7 +119,7 @@ from src.app_helpers.pipeline_table import (
119
  )
120
  from src.app_helpers.queues import filter_failed_quant_df, refresh_pipeline_leaderboard, refresh_queue_tables
121
  from src.app_helpers.sidebar import render_sidebar_menu
122
- from src.app_helpers.submissions import submit_model, submit_quant
123
  from src.app_helpers.zip_stream import register_routes as register_zip_route
124
 
125
 
@@ -529,9 +529,9 @@ with demo:
529
  elem_id="quant-scheme-select",
530
  )
531
  quant_method = gr.Dropdown(
532
- choices=["RTN (Round-To-Nearest)", "Tuning (AutoRound, recommended)"],
533
  label="Quantization Method",
534
- info='Tuning uses AutoRound calibration (default iters=200) to significantly improve quantized model accuracy. <a href="https://github.com/intel/auto-round" target="_blank" rel="noopener">Details β†—</a>',
535
  multiselect=False,
536
  value="RTN (Round-To-Nearest)",
537
  interactive=True,
@@ -567,6 +567,36 @@ with demo:
567
  interactive=True,
568
  elem_id="cuda-devices-quant",
569
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
  with gr.Column(scale=1, min_width=160, elem_classes=["submit-col"]):
571
  submit_button = gr.Button(
572
  "Submit",
@@ -580,6 +610,11 @@ with demo:
580
  elem_classes=["submit-star-nudge"],
581
  )
582
  submission_result = gr.Markdown(visible=False)
 
 
 
 
 
583
  dup_inline_quant = gr.Markdown(
584
  visible=False,
585
  elem_id="dup-inline-quant",
@@ -638,9 +673,12 @@ with demo:
638
  gpu_count_quant,
639
  container_disk_quant,
640
  cuda_devices_quant,
 
 
 
641
  ],
642
  [submission_result, model_name_textbox, submit_button, dup_inline_quant, license_warning_quant],
643
- js="""(signal, model, revision, priv, scheme, method, size, hw, cnt, disk, cuda) => {
644
  // Only clear pending_submit when NOT redirecting to OAuth
645
  if (signal !== 'TRIGGER') sessionStorage.removeItem('pending_submit');
646
  // Fallback: read model from DOM if Gradio internal state is empty
@@ -649,10 +687,17 @@ with demo:
649
  var input = document.querySelector('#model-input-quant input, #model-input-quant textarea');
650
  if (input && input.value.trim()) model = input.value.trim();
651
  }
652
- return [signal, model, revision, priv, scheme, method, size, hw, cnt, disk, cuda];
653
  }""",
654
  ) # revision_name_textbox is hidden, always "main"
655
 
 
 
 
 
 
 
 
656
  with gr.Column():
657
  with gr.Row(elem_classes=["filter-row"]):
658
  my_submissions_quant_cb = gr.Checkbox(
 
86
  )
87
  from src.populate import get_auto_pipeline_results_df, get_evaluation_queue_df
88
  from src.scripts.update_all_request_files import update_dynamic_files
89
+ from src.submission.check_validity import GPU_COUNT_CHOICES, GPU_HARDWARE_CHOICES, CONTAINER_DISK_SIZE_CHOICES, EXPORT_FORMAT_CHOICES
90
  from src.tools.plots import (
91
  compute_metrics_summary,
92
  create_scheme_accuracy_chart,
 
119
  )
120
  from src.app_helpers.queues import filter_failed_quant_df, refresh_pipeline_leaderboard, refresh_queue_tables
121
  from src.app_helpers.sidebar import render_sidebar_menu
122
+ from src.app_helpers.submissions import submit_model, submit_quant, analyze_model
123
  from src.app_helpers.zip_stream import register_routes as register_zip_route
124
 
125
 
 
529
  elem_id="quant-scheme-select",
530
  )
531
  quant_method = gr.Dropdown(
532
+ choices=["RTN (Round-To-Nearest)", "Tuning (AutoRound, recommended)", "Model-Free (RTN, no calibration)"],
533
  label="Quantization Method",
534
+ info='Tuning uses AutoRound calibration (default iters=200) to significantly improve quantized model accuracy. Model-Free is a fast weight-only RTN path for weight-only schemes (W4A16/MXFP4/MXFP8), restricted to whitelisted users. <a href="https://github.com/intel/auto-round" target="_blank" rel="noopener">Details β†—</a>',
535
  multiselect=False,
536
  value="RTN (Round-To-Nearest)",
537
  interactive=True,
 
567
  interactive=True,
568
  elem_id="cuda-devices-quant",
569
  )
570
+ export_format_quant = gr.Dropdown(
571
+ choices=EXPORT_FORMAT_CHOICES,
572
+ label="Export Format",
573
+ value=EXPORT_FORMAT_CHOICES[0],
574
+ info="Quantized model save format. Auto = auto_round.",
575
+ interactive=True,
576
+ elem_id="export-format-quant",
577
+ )
578
+ ignore_layers_quant = gr.Textbox(
579
+ label="Ignore Layers (advanced)",
580
+ placeholder="e.g. lm_head,block_sparse_moe.gate,self_attn",
581
+ info="Comma-separated module substrings to skip. Empty = recommended defaults. Overrides defaults when set.",
582
+ value="",
583
+ interactive=True,
584
+ elem_id="ignore-layers-quant",
585
+ )
586
+ layer_config_quant = gr.Textbox(
587
+ label="Layer Config β€” mixed precision (advanced)",
588
+ placeholder="e.g. {block_sparse_moe.experts:{bits:4,data_type:mx_fp}}",
589
+ info="auto-round layer_config JSON for per-module precision. Empty = uniform scheme.",
590
+ value="",
591
+ interactive=True,
592
+ elem_id="layer-config-quant",
593
+ )
594
+ analyze_button_quant = gr.Button(
595
+ "πŸ” Analyze Structure",
596
+ variant="secondary",
597
+ interactive=True,
598
+ elem_id="analyze-button-quant",
599
+ )
600
  with gr.Column(scale=1, min_width=160, elem_classes=["submit-col"]):
601
  submit_button = gr.Button(
602
  "Submit",
 
610
  elem_classes=["submit-star-nudge"],
611
  )
612
  submission_result = gr.Markdown(visible=False)
613
+ analysis_result_quant = gr.Markdown(
614
+ visible=False,
615
+ elem_id="analysis-result-quant",
616
+ elem_classes=["analysis-panel"],
617
+ )
618
  dup_inline_quant = gr.Markdown(
619
  visible=False,
620
  elem_id="dup-inline-quant",
 
673
  gpu_count_quant,
674
  container_disk_quant,
675
  cuda_devices_quant,
676
+ export_format_quant,
677
+ ignore_layers_quant,
678
+ layer_config_quant,
679
  ],
680
  [submission_result, model_name_textbox, submit_button, dup_inline_quant, license_warning_quant],
681
+ js="""(signal, model, revision, priv, scheme, method, size, hw, cnt, disk, cuda, expfmt, ignore, layercfg) => {
682
  // Only clear pending_submit when NOT redirecting to OAuth
683
  if (signal !== 'TRIGGER') sessionStorage.removeItem('pending_submit');
684
  // Fallback: read model from DOM if Gradio internal state is empty
 
687
  var input = document.querySelector('#model-input-quant input, #model-input-quant textarea');
688
  if (input && input.value.trim()) model = input.value.trim();
689
  }
690
+ return [signal, model, revision, priv, scheme, method, size, hw, cnt, disk, cuda, expfmt, ignore, layercfg];
691
  }""",
692
  ) # revision_name_textbox is hidden, always "main"
693
 
694
+ # Analyze model structure β†’ show report + pre-fill ignore/layer_config suggestions.
695
+ analyze_button_quant.click(
696
+ fn=analyze_model,
697
+ inputs=[model_name_textbox, ignore_layers_quant, layer_config_quant],
698
+ outputs=[analysis_result_quant, ignore_layers_quant, layer_config_quant],
699
+ )
700
+
701
  with gr.Column():
702
  with gr.Row(elem_classes=["filter-row"]):
703
  my_submissions_quant_cb = gr.Checkbox(
src/app_helpers/submissions.py CHANGED
@@ -15,7 +15,12 @@ from src.app_helpers.auth import check_intel_org, is_whitelisted
15
  from src.display.formatting import styled_error
16
  from src.envs import SIZE_WHITELIST, WHITELIST
17
  from src.submission.submit import add_new_eval, add_new_quant
18
- from src.submission.check_validity import is_b200_hardware, validate_cuda_visible_devices
 
 
 
 
 
19
 
20
  logger = logging.getLogger(__name__)
21
 
@@ -145,6 +150,37 @@ def _resolve_overrides(oauth_token, hardware_choice, gpu_count_choice, container
145
  return hw_override, count_override, disk_override, cuda_devices
146
 
147
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  def _empty_result():
149
  """Five outputs that signal "no progress" to the streaming UI."""
150
  return "", gr.update(), gr.update(interactive=True), gr.update(), gr.update()
@@ -252,6 +288,9 @@ def submit_quant(
252
  signal, model, revision, private, quant_scheme, quant_method_choice, model_size_input,
253
  hardware_choice, gpu_count_choice, container_disk_size_choice,
254
  cuda_visible_devices_choice=None,
 
 
 
255
  oauth_token: gr.OAuthToken | None = None,
256
  ):
257
  """Wrapper that parses the optional model-size field and calls
@@ -272,8 +311,35 @@ def submit_quant(
272
  oauth_token, hardware_choice, gpu_count_choice, container_disk_size_choice,
273
  cuda_visible_devices_choice)
274
 
275
- # Parse method choice β†’ "RTN" or "TUNING"
276
- if quant_method_choice and "Tuning" in quant_method_choice:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  method_override = "TUNING"
278
  else:
279
  method_override = "RTN"
@@ -329,6 +395,9 @@ def submit_quant(
329
  gpu_count_override=count_override,
330
  container_disk_size=disk_override,
331
  cuda_visible_devices=cuda_devices,
 
 
 
332
  submitted_by=username,
333
  submitted_orgs=orgs,
334
  user_token=oauth_token.token if oauth_token else None,
 
15
  from src.display.formatting import styled_error
16
  from src.envs import SIZE_WHITELIST, WHITELIST
17
  from src.submission.submit import add_new_eval, add_new_quant
18
+ from src.submission.check_validity import (
19
+ is_b200_hardware, validate_cuda_visible_devices, resolve_export_format,
20
+ is_model_free_supported_scheme, MODEL_FREE_METHOD,
21
+ validate_ignore_layers, validate_layer_config,
22
+ )
23
+ from src.submission.model_analysis import analyze_model_structure, render_analysis_markdown
24
 
25
  logger = logging.getLogger(__name__)
26
 
 
150
  return hw_override, count_override, disk_override, cuda_devices
151
 
152
 
153
+ def analyze_model(model, ignore_current=None, layercfg_current=None,
154
+ oauth_token: gr.OAuthToken | None = None):
155
+ """Button handler: analyze a model's structure and show it in the UI.
156
+
157
+ Returns a 3-tuple of Gradio updates for
158
+ ``[analysis_panel, ignore_layers_textbox, layer_config_textbox]``.
159
+ The recommended ignore/layer_config are pre-filled ONLY when the target
160
+ textbox is currently empty, so a user's own edits are never clobbered.
161
+ Read-only and safe; gated in the UI to whitelisted users.
162
+ """
163
+ if not model or not model.strip():
164
+ return gr.update(value="Please enter a model name first.", visible=True), gr.update(), gr.update()
165
+
166
+ token = oauth_token.token if oauth_token else None
167
+ try:
168
+ res = analyze_model_structure(model.strip(), token=token)
169
+ md = render_analysis_markdown(res)
170
+ except Exception as e: # never let the button crash the app
171
+ logger.warning("[analyze_model] failed for %s: %s", model, e)
172
+ return gr.update(value=f"Analysis error: {e}", visible=True), gr.update(), gr.update()
173
+
174
+ ign_upd = gr.update()
175
+ lc_upd = gr.update()
176
+ if res.ok:
177
+ if (not ignore_current or not ignore_current.strip()) and res.recommended_ignore_layers:
178
+ ign_upd = gr.update(value=res.recommended_ignore_layers)
179
+ if (not layercfg_current or not layercfg_current.strip()) and res.recommended_layer_config:
180
+ lc_upd = gr.update(value=res.recommended_layer_config)
181
+ return gr.update(value=md, visible=True), ign_upd, lc_upd
182
+
183
+
184
  def _empty_result():
185
  """Five outputs that signal "no progress" to the streaming UI."""
186
  return "", gr.update(), gr.update(interactive=True), gr.update(), gr.update()
 
288
  signal, model, revision, private, quant_scheme, quant_method_choice, model_size_input,
289
  hardware_choice, gpu_count_choice, container_disk_size_choice,
290
  cuda_visible_devices_choice=None,
291
+ export_format_choice=None,
292
+ ignore_layers_choice=None,
293
+ layer_config_choice=None,
294
  oauth_token: gr.OAuthToken | None = None,
295
  ):
296
  """Wrapper that parses the optional model-size field and calls
 
311
  oauth_token, hardware_choice, gpu_count_choice, container_disk_size_choice,
312
  cuda_visible_devices_choice)
313
 
314
+ # Advanced quant-only options are whitelist-gated. Non-whitelist users'
315
+ # selections are ignored (fields omitted β†’ pipeline defaults).
316
+ export_format = None
317
+ ignore_layers = None
318
+ layer_config = None
319
+ if is_whitelisted(oauth_token):
320
+ export_format = resolve_export_format(export_format_choice)
321
+ ignore_layers, ign_err = validate_ignore_layers(ignore_layers_choice)
322
+ if ign_err:
323
+ raise gr.Error(f"Invalid Ignore Layers: {ign_err}")
324
+ layer_config, lc_err = validate_layer_config(layer_config_choice)
325
+ if lc_err:
326
+ raise gr.Error(f"Invalid Layer Config: {lc_err}")
327
+
328
+ # Parse method choice β†’ "RTN" | "TUNING" | "MODEL_FREE"
329
+ if quant_method_choice and "Model-Free" in quant_method_choice:
330
+ # Model-Free is restricted to whitelisted users and weight-only schemes.
331
+ if not is_whitelisted(oauth_token):
332
+ raise gr.Error(
333
+ "Model-Free quantization is restricted to whitelisted users. "
334
+ "Please choose RTN or Tuning."
335
+ )
336
+ if not is_model_free_supported_scheme(quant_scheme):
337
+ raise gr.Error(
338
+ f"Model-Free does not support scheme '{quant_scheme}'. "
339
+ "Supported: INT4 (W4A16), MXFP4, MXFP8 (weight-only only; NVFP4/FP8 not supported)."
340
+ )
341
+ method_override = MODEL_FREE_METHOD
342
+ elif quant_method_choice and "Tuning" in quant_method_choice:
343
  method_override = "TUNING"
344
  else:
345
  method_override = "RTN"
 
395
  gpu_count_override=count_override,
396
  container_disk_size=disk_override,
397
  cuda_visible_devices=cuda_devices,
398
+ export_format=export_format,
399
+ ignore_layers=ignore_layers,
400
+ layer_config=layer_config,
401
  submitted_by=username,
402
  submitted_orgs=orgs,
403
  user_token=oauth_token.token if oauth_token else None,
src/submission/check_validity.py CHANGED
@@ -723,6 +723,139 @@ GPU_COUNT_CHOICES: list[str] = ["Auto", "1", "2", "4", "8"]
723
  # Container disk size choices for whitelisted users (GB).
724
  CONTAINER_DISK_SIZE_CHOICES: list[str] = ["Auto", "200", "400", "600", "800", "1000", "1500", "2000"]
725
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
726
  # Reverse lookup: display name β†’ short name
727
  _GPU_DISPLAY_TO_SHORT: dict[str, str] = {v: k for k, v in GPU_DISPLAY_NAMES.items()}
728
 
 
723
  # Container disk size choices for whitelisted users (GB).
724
  CONTAINER_DISK_SIZE_CHOICES: list[str] = ["Auto", "200", "400", "600", "800", "1000", "1500", "2000"]
725
 
726
+ # Export format choices for whitelisted users (auto-round save_quantized format).
727
+ # "Auto" defers to the pipeline default (auto_round).
728
+ EXPORT_FORMAT_AUTO = "Auto (auto_round)"
729
+ EXPORT_FORMAT_CHOICES: list[str] = [EXPORT_FORMAT_AUTO, "auto_round", "llm_compressor"]
730
+
731
+
732
+ def resolve_export_format(choice: str | None) -> str | None:
733
+ """Map an Export Format UI choice to an entry value.
734
+
735
+ Returns ``None`` for the "Auto" default (so the entry omits the field and
736
+ the pipeline uses its built-in default), else the concrete format string.
737
+ """
738
+ if not choice or choice.startswith("Auto"):
739
+ return None
740
+ if choice in ("auto_round", "llm_compressor"):
741
+ return choice
742
+ return None
743
+
744
+
745
+ # ── Model-Free quantization method ───────────────────────────────────────
746
+ # auto-round model-free is weight-only RTN and only supports these schemes
747
+ # (see auto_round/compressors/model_free.py). NVFP4/FP8/GGUF are NOT supported.
748
+ MODEL_FREE_METHOD = "MODEL_FREE"
749
+ _MODEL_FREE_SUPPORTED_SCHEMES = {"W2A16", "W4A16", "W8A16", "MXFP4", "MXFP8"}
750
+
751
+ # UI scheme label β†’ normalized scheme name (mirrors auto.sh's scheme_map).
752
+ _UI_SCHEME_TO_NORMALIZED = {
753
+ "INT4 (W4A16)": "W4A16",
754
+ "INT8 (W8A16)": "W8A16",
755
+ "INT4 (W4A8)": "W4A8",
756
+ "MXFP4": "MXFP4",
757
+ "MXFP8": "MXFP8",
758
+ "NVFP4": "NVFP4",
759
+ }
760
+
761
+
762
+ def normalize_scheme_name(scheme_ui: str | None) -> str:
763
+ """Normalize a UI scheme label (e.g. 'INT4 (W4A16)') to 'W4A16'."""
764
+ if not scheme_ui:
765
+ return ""
766
+ s = scheme_ui.strip()
767
+ return _UI_SCHEME_TO_NORMALIZED.get(s, s)
768
+
769
+
770
+ def is_model_free_supported_scheme(scheme_ui: str | None) -> bool:
771
+ """Whether *scheme_ui* is a weight-only scheme supported by model-free."""
772
+ return normalize_scheme_name(scheme_ui) in _MODEL_FREE_SUPPORTED_SCHEMES
773
+
774
+
775
+ # ── Custom ignore layers & mixed-precision layer_config ──────────────────
776
+ # Ignore-layer tokens are module-name substrings, e.g.
777
+ # "lm_head,block_sparse_moe.gate,self_attn.index_q_proj". Allow only characters
778
+ # that appear in module names + wildcard, and reject anything shell-hostile
779
+ # (these values are forwarded into the quantize command line downstream).
780
+ _IGNORE_TOKEN_RE = re.compile(r"^[A-Za-z0-9_.*\-]+$")
781
+ # layer_config is auto-round's relaxed JSON, e.g.
782
+ # "{block_sparse_moe.experts:{bits:4,data_type:mx_fp}}". We do a syntactic
783
+ # sanity check here; auto-round does the authoritative parse at runtime.
784
+ _LAYER_CONFIG_RE = re.compile(r"^[A-Za-z0-9_.,:{}\s\"'\-]+$")
785
+ _MAX_IGNORE_TOKENS = 64
786
+ _MAX_LAYER_CONFIG_LEN = 2000
787
+
788
+
789
+ def validate_ignore_layers(raw: str | None) -> tuple[str | None, str | None]:
790
+ """Validate/normalize a comma-separated ignore-layers list.
791
+
792
+ Returns ``(normalized, error)`` with exactly one non-None.
793
+ - empty/None β†’ ``(None, None)`` (use pipeline defaults)
794
+ - each token must match ``[A-Za-z0-9_.*-]+``; duplicates are dropped
795
+ """
796
+ if raw is None:
797
+ return None, None
798
+ text = raw.strip()
799
+ if text == "":
800
+ return None, None
801
+ tokens = [t.strip() for t in text.split(",")]
802
+ out: list[str] = []
803
+ seen: set[str] = set()
804
+ for tok in tokens:
805
+ if tok == "":
806
+ continue # tolerate stray/trailing commas
807
+ if not _IGNORE_TOKEN_RE.match(tok):
808
+ return None, (
809
+ f"Invalid ignore-layer token '{tok}': only letters, digits, '_', '.', "
810
+ "'*' and '-' are allowed."
811
+ )
812
+ if tok not in seen:
813
+ seen.add(tok)
814
+ out.append(tok)
815
+ if not out:
816
+ return None, None
817
+ if len(out) > _MAX_IGNORE_TOKENS:
818
+ return None, f"Too many ignore-layer tokens (max {_MAX_IGNORE_TOKENS})."
819
+ return ",".join(out), None
820
+
821
+
822
+ def validate_layer_config(raw: str | None) -> tuple[str | None, str | None]:
823
+ """Syntactically validate a mixed-precision ``layer_config`` string.
824
+
825
+ Accepts auto-round's relaxed JSON, e.g.
826
+ ``{block_sparse_moe.experts:{bits:4,data_type:mx_fp}}``. This is a sanity
827
+ check only (balanced braces + safe charset); auto-round performs the real
828
+ parse via ``parse_layer_config_arg`` at quantization time.
829
+
830
+ Returns ``(normalized, error)`` with exactly one non-None.
831
+ """
832
+ if raw is None:
833
+ return None, None
834
+ text = raw.strip()
835
+ if text == "":
836
+ return None, None
837
+ if len(text) > _MAX_LAYER_CONFIG_LEN:
838
+ return None, f"layer_config too long (max {_MAX_LAYER_CONFIG_LEN} chars)."
839
+ if not (text.startswith("{") and text.endswith("}")):
840
+ return None, "layer_config must be a JSON-like object wrapped in { }."
841
+ if not _LAYER_CONFIG_RE.match(text):
842
+ return None, (
843
+ "layer_config contains disallowed characters. Use only module names, "
844
+ "digits, and { } : , . _ - (e.g. {experts:{bits:4,data_type:mx_fp}})."
845
+ )
846
+ # Balanced-brace check
847
+ depth = 0
848
+ for ch in text:
849
+ if ch == "{":
850
+ depth += 1
851
+ elif ch == "}":
852
+ depth -= 1
853
+ if depth < 0:
854
+ return None, "layer_config has unbalanced braces."
855
+ if depth != 0:
856
+ return None, "layer_config has unbalanced braces."
857
+ return text, None
858
+
859
  # Reverse lookup: display name β†’ short name
860
  _GPU_DISPLAY_TO_SHORT: dict[str, str] = {v: k for k, v in GPU_DISPLAY_NAMES.items()}
861
 
src/submission/model_analysis.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright and license: follows the repository's existing convention.
2
+ """Download-free model structure & parameter analysis for quantization planning.
3
+
4
+ Given a HuggingFace model id, this inspects the model's ``config.json`` and the
5
+ **safetensors headers only** (no weight download) to produce:
6
+
7
+ * a per-category parameter distribution (MoE experts, shared experts, attention,
8
+ router/gate, dense MLP, lm_head, embeddings, vision, norms, …), matching the
9
+ format of the mixed-precision reference docs, and
10
+ * recommended ``ignore_layers`` / mixed-precision ``layer_config`` presets a user
11
+ can drop straight into the advanced submission fields.
12
+
13
+ The heavy lifting uses ``huggingface_hub.get_safetensors_metadata`` which reads
14
+ only the safetensors headers (tensor name + shape + dtype), so even trillion-param
15
+ models are analyzed without downloading weights.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import re
21
+ from dataclasses import dataclass, field
22
+
23
+ from huggingface_hub import get_safetensors_metadata
24
+ from transformers import AutoConfig
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # Collapse per-layer indices so ``...layers.12.self_attn.q_proj.weight`` and
29
+ # ``...layers.0.self_attn.q_proj.weight`` bucket together.
30
+ _LAYER_IDX_RE = re.compile(r"\.\d+\.")
31
+
32
+
33
+ @dataclass
34
+ class CategoryStat:
35
+ name: str
36
+ params: int = 0
37
+ tensors_2d: int = 0
38
+ tensors_total: int = 0
39
+
40
+
41
+ @dataclass
42
+ class ModelAnalysis:
43
+ model_id: str
44
+ ok: bool = True
45
+ error: str | None = None
46
+ architectures: list[str] = field(default_factory=list)
47
+ model_type: str = ""
48
+ hidden_size: int | None = None
49
+ num_layers: int | None = None
50
+ num_experts: int | None = None
51
+ vocab_size: int | None = None
52
+ total_params: int = 0
53
+ is_moe: bool = False
54
+ has_shared_experts: bool = False
55
+ has_attn_indexer: bool = False
56
+ has_vision: bool = False
57
+ approximate: bool = False
58
+ categories: list[CategoryStat] = field(default_factory=list)
59
+ recommended_ignore_layers: str = ""
60
+ recommended_layer_config: str = ""
61
+
62
+
63
+ # ── Categorization ────────────────────────────────────────────────────────
64
+ # Order matters: the first matching rule wins. Each rule is (category, predicate).
65
+ def _categorize(norm_name: str) -> str:
66
+ n = norm_name
67
+ # Routers / gates first (must beat gate_proj which is a normal MLP proj).
68
+ if (".gate." in n or n.endswith(".gate") or n.endswith(".gate.weight")
69
+ or ".router" in n or n.endswith(".router")) and "gate_proj" not in n and "gate_up_proj" not in n:
70
+ return "router_gate"
71
+ if "shared_expert" in n:
72
+ return "shared_experts"
73
+ if ".experts." in n or n.endswith(".experts") or ".block_sparse_moe.experts" in n:
74
+ return "moe_experts"
75
+ if "vision" in n or "visual" in n or "vit" in n:
76
+ return "vision"
77
+ if "lm_head" in n:
78
+ return "lm_head"
79
+ if "embed" in n or "wte" in n or "word_embeddings" in n:
80
+ return "embeddings"
81
+ if "self_attn" in n or ".attn." in n or "attention" in n:
82
+ if "index" in n or "indexer" in n:
83
+ return "attn_indexer"
84
+ if "norm" in n:
85
+ return "norm"
86
+ return "self_attn"
87
+ if "mtp" in n:
88
+ return "mtp"
89
+ if "projector" in n or "patch_merge" in n or "multi_modal" in n:
90
+ return "projector"
91
+ if "mlp" in n or "feed_forward" in n or "ffn" in n:
92
+ return "dense_mlp"
93
+ if "norm" in n or "layernorm" in n or "ln_" in n:
94
+ return "norm"
95
+ return "other"
96
+
97
+
98
+ _CATEGORY_ORDER = [
99
+ "moe_experts", "shared_experts", "dense_mlp", "self_attn", "attn_indexer",
100
+ "router_gate", "mtp", "vision", "projector", "lm_head", "embeddings",
101
+ "norm", "other",
102
+ ]
103
+
104
+ _CATEGORY_LABELS = {
105
+ "moe_experts": "MoE routed experts",
106
+ "shared_experts": "MoE shared experts",
107
+ "dense_mlp": "Dense MLP",
108
+ "self_attn": "Attention (q/k/v/o)",
109
+ "attn_indexer": "Attention indexer (sparse)",
110
+ "router_gate": "Router / gate",
111
+ "mtp": "MTP module",
112
+ "vision": "Vision tower",
113
+ "projector": "Projector / merger",
114
+ "lm_head": "lm_head",
115
+ "embeddings": "Embeddings",
116
+ "norm": "Norms (1D, auto-skipped)",
117
+ "other": "Other",
118
+ }
119
+
120
+
121
+ def _cfg_get(cfg, *attrs, default=None):
122
+ """Read the first present attribute from a config or its nested text_config."""
123
+ sources = [cfg]
124
+ if hasattr(cfg, "text_config") and cfg.text_config is not None:
125
+ sources.append(cfg.text_config)
126
+ for src in sources:
127
+ for a in attrs:
128
+ v = getattr(src, a, None)
129
+ if v is not None:
130
+ return v
131
+ return default
132
+
133
+
134
+ def analyze_model_structure(model_id: str, revision: str = "main", token: str | None = None) -> ModelAnalysis:
135
+ """Analyze *model_id* without downloading weights. Never raises."""
136
+ res = ModelAnalysis(model_id=model_id)
137
+
138
+ # 1. Config (best-effort; some fields inform MoE detection & the report).
139
+ try:
140
+ cfg = AutoConfig.from_pretrained(model_id, revision=revision, token=token, trust_remote_code=True)
141
+ arch = getattr(cfg, "architectures", None) or []
142
+ res.architectures = list(arch)
143
+ res.model_type = getattr(cfg, "model_type", "") or ""
144
+ res.hidden_size = _cfg_get(cfg, "hidden_size", "n_embd", "d_model")
145
+ res.num_layers = _cfg_get(cfg, "num_hidden_layers", "n_layer", "num_layers")
146
+ res.num_experts = _cfg_get(cfg, "num_experts", "num_local_experts", "n_routed_experts", "moe_num_experts")
147
+ res.vocab_size = _cfg_get(cfg, "vocab_size")
148
+ except Exception as e:
149
+ logger.warning("[analyze] config load failed for %s: %s", model_id, e)
150
+
151
+ # 2. Per-tensor metadata from safetensors headers (no weight download).
152
+ try:
153
+ meta = get_safetensors_metadata(model_id, revision=revision, token=token)
154
+ except Exception as e:
155
+ res.ok = False
156
+ res.error = (
157
+ f"Could not read safetensors metadata: {e}. "
158
+ "The model may lack safetensors (e.g. GGUF/pytorch_model.bin) or be gated/private."
159
+ )
160
+ return res
161
+
162
+ # Map filename β†’ {tensor_name: TensorInfo} for shapes/dtypes/param counts.
163
+ cats: dict[str, CategoryStat] = {c: CategoryStat(c) for c in _CATEGORY_ORDER}
164
+ total = 0
165
+ for fname, fmeta in (meta.files_metadata or {}).items():
166
+ for tname, tinfo in (fmeta.tensors or {}).items():
167
+ pc = int(getattr(tinfo, "parameter_count", 0) or 0)
168
+ shape = list(getattr(tinfo, "shape", []) or [])
169
+ norm = _LAYER_IDX_RE.sub(".N.", tname)
170
+ cat = _categorize(norm)
171
+ cs = cats[cat]
172
+ cs.params += pc
173
+ cs.tensors_total += 1
174
+ if len(shape) == 2:
175
+ cs.tensors_2d += 1
176
+ total += pc
177
+
178
+ res.total_params = total
179
+ res.categories = [cats[c] for c in _CATEGORY_ORDER if cats[c].tensors_total > 0]
180
+
181
+ # 3. Derived traits.
182
+ res.is_moe = cats["moe_experts"].tensors_total > 0 or bool(res.num_experts)
183
+ res.has_shared_experts = cats["shared_experts"].tensors_total > 0
184
+ res.has_attn_indexer = cats["attn_indexer"].tensors_total > 0
185
+ res.has_vision = cats["vision"].tensors_total > 0
186
+
187
+ # 4. Recommendations.
188
+ res.recommended_ignore_layers, res.recommended_layer_config = _recommend(res, cats)
189
+ return res
190
+
191
+
192
+ def _recommend(res: ModelAnalysis, cats: dict[str, CategoryStat]) -> tuple[str, str]:
193
+ """Produce a recommended ignore_layers list + mixed-precision layer_config.
194
+
195
+ Heuristic (from the reference mixed-precision docs):
196
+ * Always skip/ignore: lm_head, embeddings (type-skipped), router/gate, norms (1D).
197
+ * Vision tower, projectors, attention indexer β†’ ignore (keep bf16).
198
+ * MoE routed experts β†’ low-bit (MXFP4); shared experts β†’ higher precision
199
+ via layer_config (MXFP8) when present.
200
+ """
201
+ ignore: list[str] = []
202
+ if cats["lm_head"].tensors_total:
203
+ ignore.append("lm_head")
204
+ if cats["router_gate"].tensors_total:
205
+ # Common router names; users can trim to their model's exact name.
206
+ ignore.append("gate")
207
+ if cats["vision"].tensors_total:
208
+ ignore.append("vision_tower")
209
+ if cats["projector"].tensors_total:
210
+ ignore.append("multi_modal_projector")
211
+ ignore.append("patch_merge")
212
+ if cats["attn_indexer"].tensors_total:
213
+ ignore.append("indexer")
214
+ # Deduplicate, preserve order.
215
+ seen = set()
216
+ ignore = [x for x in ignore if not (x in seen or seen.add(x))]
217
+
218
+ layer_config = ""
219
+ if res.is_moe and cats["moe_experts"].tensors_total:
220
+ # Route the routed experts to MXFP4; leave the rest at the global scheme.
221
+ # 'experts' matches routed experts without touching 'shared_experts'.
222
+ layer_config = "{experts:{bits:4,data_type:mx_fp}}"
223
+
224
+ return ",".join(ignore), layer_config
225
+
226
+
227
+ # ── Markdown rendering ─────────────────────────────────────────────────────
228
+ def _fmt_b(params: int) -> str:
229
+ if params >= 1e9:
230
+ return f"{params / 1e9:.2f} B"
231
+ if params >= 1e6:
232
+ return f"{params / 1e6:.2f} M"
233
+ return str(params)
234
+
235
+
236
+ def render_analysis_markdown(res: ModelAnalysis) -> str:
237
+ if not res.ok:
238
+ return f"### ⚠️ Model structure analysis failed\n\n{res.error}"
239
+
240
+ lines: list[str] = []
241
+ lines.append(f"### πŸ” Model structure β€” `{res.model_id}`\n")
242
+
243
+ # Overview
244
+ lines.append("| Field | Value |")
245
+ lines.append("|---|---|")
246
+ if res.architectures:
247
+ lines.append(f"| Architecture | `{', '.join(res.architectures)}` |")
248
+ if res.model_type:
249
+ lines.append(f"| model_type | `{res.model_type}` |")
250
+ lines.append(f"| Total parameters (measured) | **{_fmt_b(res.total_params)}** |")
251
+ if res.num_layers is not None:
252
+ lines.append(f"| Layers | {res.num_layers} |")
253
+ if res.num_experts:
254
+ lines.append(f"| Experts | {res.num_experts} |")
255
+ if res.hidden_size is not None:
256
+ lines.append(f"| hidden_size | {res.hidden_size} |")
257
+ if res.vocab_size is not None:
258
+ lines.append(f"| vocab_size | {res.vocab_size} |")
259
+ traits = []
260
+ if res.is_moe:
261
+ traits.append("MoE")
262
+ if res.has_shared_experts:
263
+ traits.append("shared-experts")
264
+ if res.has_attn_indexer:
265
+ traits.append("sparse-attn-indexer")
266
+ if res.has_vision:
267
+ traits.append("vision")
268
+ if traits:
269
+ lines.append(f"| Traits | {', '.join(traits)} |")
270
+ lines.append("")
271
+
272
+ # Parameter distribution
273
+ lines.append("#### Parameter distribution by module category\n")
274
+ lines.append("| Category | Params | Share | 2D-linear tensors |")
275
+ lines.append("|---|---:|---:|---:|")
276
+ total = res.total_params or 1
277
+ for cs in sorted(res.categories, key=lambda c: c.params, reverse=True):
278
+ share = 100.0 * cs.params / total
279
+ label = _CATEGORY_LABELS.get(cs.name, cs.name)
280
+ lines.append(f"| {label} | {_fmt_b(cs.params)} | {share:.2f}% | {cs.tensors_2d} |")
281
+ lines.append("")
282
+
283
+ # Recommendations
284
+ lines.append("#### Recommended quantization controls\n")
285
+ if res.recommended_ignore_layers:
286
+ lines.append(f"- **Ignore Layers:** `{res.recommended_ignore_layers}`")
287
+ else:
288
+ lines.append("- **Ignore Layers:** *(defaults are fine β€” nothing extra to skip)*")
289
+ if res.recommended_layer_config:
290
+ lines.append(f"- **Layer Config (mixed precision):** `{res.recommended_layer_config}`")
291
+ lines.append(" - routes MoE routed experts to MXFP4 while the rest stay at the global scheme.")
292
+ lines.append("")
293
+ lines.append("> These are suggestions β€” review against your model's exact module names before submitting. "
294
+ "Norms (1D) and embeddings are skipped automatically by AutoRound.")
295
+ return "\n".join(lines)
src/submission/submit.py CHANGED
@@ -722,6 +722,9 @@ def add_new_quant(
722
  gpu_count_override: int | None = None,
723
  container_disk_size: str | None = None,
724
  cuda_visible_devices: str | None = None,
 
 
 
725
  submitted_by: str = "",
726
  submitted_orgs: list[str] | None = None,
727
  user_token: str | None = None,
@@ -964,6 +967,12 @@ def add_new_quant(
964
  }
965
  if container_disk_size:
966
  quant_entry["container_disk_size"] = container_disk_size
 
 
 
 
 
 
967
  _apply_b200_local_fields(quant_entry, quant_gpu_type, cuda_visible_devices)
968
 
969
  supplementary = _get_supplementary_info(
 
722
  gpu_count_override: int | None = None,
723
  container_disk_size: str | None = None,
724
  cuda_visible_devices: str | None = None,
725
+ export_format: str | None = None,
726
+ ignore_layers: str | None = None,
727
+ layer_config: str | None = None,
728
  submitted_by: str = "",
729
  submitted_orgs: list[str] | None = None,
730
  user_token: str | None = None,
 
967
  }
968
  if container_disk_size:
969
  quant_entry["container_disk_size"] = container_disk_size
970
+ if export_format:
971
+ quant_entry["export_format"] = export_format
972
+ if ignore_layers:
973
+ quant_entry["ignore_layers"] = ignore_layers
974
+ if layer_config:
975
+ quant_entry["layer_config"] = layer_config
976
  _apply_b200_local_fields(quant_entry, quant_gpu_type, cuda_visible_devices)
977
 
978
  supplementary = _get_supplementary_info(