@@ -153,6 +153,42 @@ class VRAMBitwidthEstimation:
153153 meta_bits_per_param : float
154154
155155
156+ @dataclass
157+ class EnvironmentSnapshot :
158+ """Physical hardware readings at check-env time."""
159+
160+ gpu_count : int
161+ gpu_name : str | None
162+ gpu_total_vram_gb : float | None
163+ gpu_free_vram_gb : float | None
164+ ram_total_gb : float | None
165+ ram_available_gb : float | None
166+ disk_available_gb : float | None
167+ disk_path : str
168+
169+
170+ @dataclass
171+ class ModelMemoryProfile :
172+ """Derived memory footprint for the target model."""
173+
174+ total_params : int
175+ fp16_gb : float
176+ quantized_gb : dict
177+ calibration_overhead_gb : float
178+
179+
180+ @dataclass
181+ class EnvCheckResult :
182+ """Composite result returned by check_environment()."""
183+
184+ model_id : str
185+ env : EnvironmentSnapshot
186+ model : ModelMemoryProfile
187+ estimation : VRAMBitwidthEstimation | None
188+ risk : str
189+ risk_detail : str
190+
191+
156192def estimate_target_bitwidth (
157193 model : torch .nn .Module ,
158194 vram_ratio : float = 0.70 ,
@@ -322,3 +358,241 @@ def estimate_wbits_from_vram(
322358 wbits = wbits ,
323359 logger = logger ,
324360 )
361+
362+
363+ def check_environment (
364+ model_id : str ,
365+ * ,
366+ total_vram_gb : float | None = None ,
367+ group_size : int = 128 ,
368+ save_dir : str | None = None ,
369+ vram_ratio : float = 0.80 ,
370+ calibration_overhead_ratio : float = 0.15 ,
371+ ) -> EnvCheckResult :
372+ """Collect hardware info and estimate OOM risk before quantization.
373+
374+ Loads the model architecture on a ``meta`` device (no GPU/CPU memory)
375+ to count parameters, then compares available VRAM against estimated
376+ memory requirements at 2/4/8-bit quantization.
377+
378+ Args:
379+ model_id: Hugging Face model ID or local path.
380+ total_vram_gb: Override GPU VRAM in GB for estimation math only.
381+ Physical GPU readings are always from the real device.
382+ group_size: GPTQ group size for metadata calculation.
383+ save_dir: Path used for disk-space check. Defaults to cwd.
384+ vram_ratio: Fraction of VRAM allocated for the estimation budget.
385+ calibration_overhead_ratio: Calibration activation buffer as a
386+ fraction of the FP16 model footprint (default 15 %).
387+
388+ Returns:
389+ :class:`EnvCheckResult` with hardware snapshot, memory profile,
390+ VRAM estimation, and risk level (``"safe"``, ``"warning"``,
391+ ``"danger"``, or ``"unknown"``).
392+ """
393+ import os
394+ import pathlib
395+ import shutil
396+
397+ from transformers import AutoConfig , AutoModelForCausalLM
398+
399+ # --- GPU snapshot --------------------------------------------------------
400+ gpu_count = torch .cuda .device_count ()
401+ if gpu_count > 0 :
402+ dev = torch .cuda .current_device ()
403+ props = torch .cuda .get_device_properties (dev )
404+ gpu_name = props .name
405+ gpu_total_vram_gb = props .total_memory / _BYTES_PER_GB
406+ try :
407+ free_bytes , _ = torch .cuda .mem_get_info (dev )
408+ gpu_free_vram_gb = free_bytes / _BYTES_PER_GB
409+ except Exception :
410+ gpu_free_vram_gb = None
411+ else :
412+ gpu_name = None
413+ gpu_total_vram_gb = None
414+ gpu_free_vram_gb = None
415+
416+ # --- CPU RAM (psutil optional) -------------------------------------------
417+ try :
418+ import psutil
419+
420+ vm = psutil .virtual_memory ()
421+ ram_total_gb = vm .total / _BYTES_PER_GB
422+ ram_available_gb = vm .available / _BYTES_PER_GB
423+ except ImportError :
424+ ram_total_gb = None
425+ ram_available_gb = None
426+
427+ # --- Disk space (stdlib) -------------------------------------------------
428+ check_path = save_dir if save_dir else os .getcwd ()
429+ p = pathlib .Path (check_path )
430+ while not p .exists ():
431+ p = p .parent
432+ disk_available_gb = shutil .disk_usage (p ).free / _BYTES_PER_GB
433+
434+ # --- Model memory profile ------------------------------------------------
435+ config = AutoConfig .from_pretrained (model_id )
436+ with torch .device ("meta" ):
437+ model = AutoModelForCausalLM .from_config (config , torch_dtype = torch .float16 )
438+
439+ total_params = sum (p .numel () for p in model .parameters ())
440+ fp16_gb = (total_params * 2 ) / _BYTES_PER_GB
441+ quantized_gb = {b : weight_memory_gb (total_params , b , group_size ) for b in (2 , 4 , 8 )}
442+ calibration_overhead_gb = fp16_gb * calibration_overhead_ratio
443+
444+ # --- VRAM bitwidth estimation (reuse existing) ---------------------------
445+ try :
446+ estimation = estimate_target_bitwidth (
447+ model ,
448+ vram_ratio = vram_ratio ,
449+ total_vram_gb = total_vram_gb ,
450+ group_size = group_size ,
451+ )
452+ except (RuntimeError , ValueError ):
453+ estimation = None
454+
455+ # --- OOM risk assessment -------------------------------------------------
456+ # Use free VRAM (runtime reality) when available; fall back to override.
457+ effective_vram = gpu_free_vram_gb if gpu_free_vram_gb is not None else total_vram_gb
458+
459+ if effective_vram is None :
460+ risk = "unknown"
461+ risk_detail = "No GPU detected and no --total-vram-gb provided."
462+ else :
463+ need_4bit = quantized_gb [4 ] + calibration_overhead_gb
464+ if effective_vram >= fp16_gb * 1.2 :
465+ risk = "safe"
466+ risk_detail = (
467+ f"Free VRAM ({ effective_vram :.1f} GB) comfortably fits "
468+ f"even FP16 weights ({ fp16_gb :.1f} GB × 1.2)."
469+ )
470+ elif effective_vram >= need_4bit :
471+ risk = "warning"
472+ risk_detail = (
473+ f"Free VRAM ({ effective_vram :.1f} GB) fits 4-bit quantized "
474+ f"weights but is tight (calibration overhead included)."
475+ )
476+ else :
477+ risk = "danger"
478+ risk_detail = (
479+ f"Free VRAM ({ effective_vram :.1f} GB) is insufficient for "
480+ f"4-bit + calibration ({ need_4bit :.1f} GB needed)."
481+ )
482+
483+ return EnvCheckResult (
484+ model_id = model_id ,
485+ env = EnvironmentSnapshot (
486+ gpu_count = gpu_count ,
487+ gpu_name = gpu_name ,
488+ gpu_total_vram_gb = gpu_total_vram_gb ,
489+ gpu_free_vram_gb = gpu_free_vram_gb ,
490+ ram_total_gb = ram_total_gb ,
491+ ram_available_gb = ram_available_gb ,
492+ disk_available_gb = disk_available_gb ,
493+ disk_path = str (p ),
494+ ),
495+ model = ModelMemoryProfile (
496+ total_params = total_params ,
497+ fp16_gb = fp16_gb ,
498+ quantized_gb = quantized_gb ,
499+ calibration_overhead_gb = calibration_overhead_gb ,
500+ ),
501+ estimation = estimation ,
502+ risk = risk ,
503+ risk_detail = risk_detail ,
504+ )
505+
506+
507+ def print_env_report (result : EnvCheckResult , * , total_vram_gb_override : float | None = None ) -> None :
508+ """Print a human-readable environment and OOM risk report to stdout.
509+
510+ Args:
511+ result: The :class:`EnvCheckResult` from :func:`check_environment`.
512+ total_vram_gb_override: When not ``None``, annotates the VRAM budget
513+ line with ``[--total-vram-gb override]``.
514+ """
515+ _W = 60
516+ _SEP = "=" * _W
517+ _COL = 22
518+
519+ def _row (label : str , value : str ) -> str :
520+ return f" { label :<{_COL }} : { value } "
521+
522+ risk_labels = {
523+ "safe" : "SAFE" ,
524+ "warning" : "WARNING" ,
525+ "danger" : "DANGER !!" ,
526+ "unknown" : "UNKNOWN" ,
527+ }
528+ risk_label = risk_labels .get (result .risk , result .risk .upper ())
529+
530+ e = result .env
531+ m = result .model
532+
533+ print (_SEP )
534+ print (" OneComp Environment Check" )
535+ print (_SEP )
536+ print ()
537+
538+ # Hardware
539+ print ("Hardware" )
540+ print (_row ("GPU count" , str (e .gpu_count )))
541+ if e .gpu_name is not None :
542+ print (_row ("GPU name" , e .gpu_name ))
543+ if e .gpu_total_vram_gb is not None :
544+ label = "GPU VRAM (total)"
545+ value = f"{ e .gpu_total_vram_gb :.1f} GB"
546+ if total_vram_gb_override is not None :
547+ value += " [physical]"
548+ print (_row (label , value ))
549+ if total_vram_gb_override is not None :
550+ print (_row ("VRAM budget used" , f"{ total_vram_gb_override :.1f} GB [--total-vram-gb override]" ))
551+ if e .gpu_free_vram_gb is not None :
552+ print (_row ("GPU VRAM (free)" , f"{ e .gpu_free_vram_gb :.1f} GB" ))
553+ if e .ram_total_gb is not None :
554+ print (_row ("CPU RAM (total)" , f"{ e .ram_total_gb :.1f} GB" ))
555+ print (_row ("CPU RAM (avail)" , f"{ e .ram_available_gb :.1f} GB" ))
556+ else :
557+ print (_row ("CPU RAM" , "n/a (install psutil for RAM info)" ))
558+ print (_row ("Disk (avail)" , f"{ e .disk_available_gb :.1f} GB [{ e .disk_path } ]" ))
559+ print ()
560+
561+ # Model
562+ print (f"Model: { result .model_id } " )
563+ print (_row ("Parameters" , f"{ m .total_params :,} " ))
564+ print (_row ("FP16 footprint" , f"{ m .fp16_gb :.2f} GB" ))
565+ print ()
566+
567+ # Memory estimates
568+ gs = "(group_size varies)"
569+ print (f"Memory Estimates" )
570+ for bits in (2 , 4 , 8 ):
571+ print (_row (f"{ bits } -bit quantized" , f"{ m .quantized_gb [bits ]:.2f} GB" ))
572+ print (_row ("Calib. overhead" , f"{ m .calibration_overhead_gb :.2f} GB (15% of FP16)" ))
573+ print (_row ("4-bit + overhead" , f"{ m .quantized_gb [4 ] + m .calibration_overhead_gb :.2f} GB" ))
574+ print ()
575+
576+ # OOM risk
577+ print ("OOM Risk Assessment" )
578+ print (_row ("Risk level" , risk_label ))
579+ detail_words = result .risk_detail .split ()
580+ detail_line = ""
581+ detail_lines = []
582+ for word in detail_words :
583+ if len (detail_line ) + len (word ) + 1 > 34 :
584+ detail_lines .append (detail_line )
585+ detail_line = word
586+ else :
587+ detail_line = (detail_line + " " + word ).lstrip ()
588+ if detail_line :
589+ detail_lines .append (detail_line )
590+ for i , dl in enumerate (detail_lines ):
591+ if i == 0 :
592+ print (_row ("Detail" , dl ))
593+ else :
594+ print (f" { '' :<{_COL }} { dl } " )
595+ print ()
596+ if result .estimation is not None :
597+ print (_row ("Recommended wbits" , f"{ result .estimation .target_bitwidth :.2f} (VRAM-estimated)" ))
598+ print (_SEP )
0 commit comments