From 9ffa8840bf133e9656abdec0f76bf63c8cdbd4a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 06:53:19 +0000 Subject: [PATCH 1/5] Fix TypeError: convert max_seq_length from float to int MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root Cause: - NiceGUI's ui.number component returns float values by default - The tokenizer's enable_truncation() expects max_length as integer - This caused: TypeError: argument 'max_length': 'float' object cannot be interpreted as an integer Changes: - Convert max_seq_length to int in QuantizationConfig.__init__ - Also convert max_calib_samples to int for consistency Impact: - Tokenization will now succeed with proper integer max_length - Both calibration parameters are guaranteed to be integers Error path before fix: UI (float) → QuantizationConfig (float) → oneshot (float) → tokenizer.enable_truncation(max_length=1024.0) → TypeError After fix: UI (float) → QuantizationConfig (int) → oneshot (int) → tokenizer.enable_truncation(max_length=1024) → Success --- src/msquant/core/quantizer/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/msquant/core/quantizer/config.py b/src/msquant/core/quantizer/config.py index c1ca636..4c69cb4 100644 --- a/src/msquant/core/quantizer/config.py +++ b/src/msquant/core/quantizer/config.py @@ -35,8 +35,8 @@ def __init__( self.calib_dataset = calib_dataset self.calib_config = calib_config self.calib_split = calib_split - self.max_calib_samples = max_calib_samples - self.max_seq_length = max_seq_length + self.max_calib_samples = int(max_calib_samples) + self.max_seq_length = int(max_seq_length) # AWQ self.w_bit = w_bit From 6e725ee18ed072566be34e4d70832f8d994ba2ad Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 06:56:00 +0000 Subject: [PATCH 2/5] Convert w_bit and group_size to integers for type safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue: - NiceGUI's ui.number component returns float values - group_size from ui.number('Group Size', ...) returns float - w_bit from ui.select should be int but converting for safety Changes: - Convert w_bit to int (line 42) - Convert group_size to int (line 43) All numeric parameters in QuantizationConfig now guaranteed to be integers: ✓ max_calib_samples (int) ✓ max_seq_length (int) ✓ w_bit (int) ✓ group_size (int) This prevents potential type errors downstream in quantization libraries that expect integer parameters. --- src/msquant/core/quantizer/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/msquant/core/quantizer/config.py b/src/msquant/core/quantizer/config.py index 4c69cb4..4fa1252 100644 --- a/src/msquant/core/quantizer/config.py +++ b/src/msquant/core/quantizer/config.py @@ -39,8 +39,8 @@ def __init__( self.max_seq_length = int(max_seq_length) # AWQ - self.w_bit = w_bit - self.group_size = group_size + self.w_bit = int(w_bit) + self.group_size = int(group_size) self.zero_point = zero_point # NVFP4 From 2a8fb51a458afe091aefa4c26bbedb6c2cead54c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 06:59:53 +0000 Subject: [PATCH 3/5] Replace input fields with editable combo boxes for better UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Dataset Split: Changed from text input to editable select Options: ['train', 'test', 'validation', 'train[:512]', 'test[:512]'] Added clearable prop (field is optional) - Max Calibration Samples: Changed from number input to editable select Options: [128, 256, 512, 1024, 2048] Default: 256 - Max Sequence Length: Changed from number input to editable select Options: [512, 1024, 2048, 4096, 8192] Default: 2048 - Group Size: Changed from number input to editable select Options: [32, 64, 128, 256] Default: 128 Benefits: ✓ Users can pick from common values via dropdown ✓ Users can still type custom values (use-input prop) ✓ new-value-mode=add-unique allows adding typed values ✓ Guided defaults prevent invalid configurations ✓ Better UX than plain number/text inputs All fields remain fully editable while providing sensible defaults. --- src/msquant/app/pages/configure.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/msquant/app/pages/configure.py b/src/msquant/app/pages/configure.py index 9ff2c04..113e39b 100644 --- a/src/msquant/app/pages/configure.py +++ b/src/msquant/app/pages/configure.py @@ -69,11 +69,23 @@ def start_quantization(): ui.label('Calibration Settings').classes('text-2xl font-bold') ui.input('Calibration Dataset', placeholder='e.g., wikitext').classes('w-full').bind_value(form_data, 'calib_dataset') ui.input('Dataset Config', placeholder='e.g., wikitext-2-raw-v1 (optional)').classes('w-full').bind_value(form_data, 'calib_config') - ui.input('Dataset Split', placeholder='e.g., train (optional)').classes('w-full').bind_value(form_data, 'calib_split') - + ui.select( + ['train', 'test', 'validation', 'train[:512]', 'test[:512]'], + value='train', + label='Dataset Split' + ).classes('w-full').bind_value(form_data, 'calib_split').props('use-input new-value-mode=add-unique clearable') + with ui.row().classes('w-full gap-4'): - ui.number('Max Calibration Samples', value=256, min=1, max=10000).classes('flex-1').bind_value(form_data, 'max_calib_samples') - ui.number('Max Sequence Length', value=2048, min=128, max=8192).classes('flex-1').bind_value(form_data, 'max_seq_length') + ui.select( + [128, 256, 512, 1024, 2048], + value=256, + label='Max Calibration Samples' + ).classes('flex-1').bind_value(form_data, 'max_calib_samples').props('use-input new-value-mode=add-unique') + ui.select( + [512, 1024, 2048, 4096, 8192], + value=2048, + label='Max Sequence Length' + ).classes('flex-1').bind_value(form_data, 'max_seq_length').props('use-input new-value-mode=add-unique') ui.separator() @@ -81,7 +93,11 @@ def start_quantization(): ui.label('AWQ Settings (only for AWQ method)').classes('text-2xl font-bold') with ui.row().classes('w-full gap-4'): ui.select([2, 3, 4, 5, 8], value=4, label='Weight Bits').classes('flex-1').bind_value(form_data, 'w_bit') - ui.number('Group Size', value=128, min=1).classes('flex-1').bind_value(form_data, 'group_size') + ui.select( + [32, 64, 128, 256], + value=128, + label='Group Size' + ).classes('flex-1').bind_value(form_data, 'group_size').props('use-input new-value-mode=add-unique') ui.checkbox('Zero Point', value=True).bind_value(form_data, 'zero_point') ui.separator() From aed858dbd5efde91563193b758304da1ac79102b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 07:03:23 +0000 Subject: [PATCH 4/5] Fix notification spam by tracking last notified status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root Cause: - ui.timer(2.0, update_status) calls update_status every 2 seconds - update_status() showed notifications every time status was 'failed', 'completed', or 'cancelled' - No check to see if notification was already shown - Result: 30+ duplicate "Job failed..." notifications Changes: - Added last_notified_status state dict to track: - Last notified status ('failed', 'completed', etc.) - Last notified message (error text, result, etc.) - Updated notification logic: - 'completed': Only notify on first completion - 'failed': Only notify on first failure OR if error message changes - 'cancelled': Only notify on first cancellation - 'idle': Reset notification state (ready for next job) Benefits: ✓ Each status change notifies exactly once ✓ Different error messages still trigger new notifications ✓ State resets when returning to idle ✓ No more notification spam ✓ User sees clean, non-repetitive status updates Before: 30+ "Job failed..." notifications every 2 seconds After: 1 "Job failed..." notification per failure --- src/msquant/app/pages/monitor.py | 38 +++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/msquant/app/pages/monitor.py b/src/msquant/app/pages/monitor.py index b207223..1e137f6 100644 --- a/src/msquant/app/pages/monitor.py +++ b/src/msquant/app/pages/monitor.py @@ -12,7 +12,10 @@ def create_monitor_page(job_service: JobService, gpu_monitor: GPUMonitor): # State for selected GPU selected_gpu_index = {"value": 0} available_gpus = {"list": []} - + + # Track last notified status to prevent notification spam + last_notified_status = {"status": None, "message": None} + with ui.column().classes('w-full p-8 gap-4'): ui.label('Job Monitor').classes('text-4xl font-bold') @@ -79,25 +82,38 @@ def update_status(): """Update job status and logs.""" status = job_service.get_status() status_label.text = f'Status: {status.value.upper()}' - + # Enable/disable cancel button based on status cancel_btn.props(f'disable={status.value != "running"}') - + # Get logs logs = job_service.get_logs(last_n=100) log_output.value = '\n'.join(logs) if logs else 'No logs yet...' - - # Show result or error - if status.value == 'completed': + + # Show result or error - but only once per status change + current_status = status.value + + if current_status == 'completed': result = job_service.get_result() - if result: + if result and last_notified_status["status"] != 'completed': ui.notify(f'Job completed! Output: {result}', type='positive') - elif status.value == 'failed': + last_notified_status["status"] = 'completed' + last_notified_status["message"] = result + elif current_status == 'failed': error = job_service.get_error() - if error: + if error and (last_notified_status["status"] != 'failed' or last_notified_status["message"] != error): ui.notify(f'Job failed: {error}', type='negative') - elif status.value == 'cancelled': - ui.notify('Job was cancelled', type='warning') + last_notified_status["status"] = 'failed' + last_notified_status["message"] = error + elif current_status == 'cancelled': + if last_notified_status["status"] != 'cancelled': + ui.notify('Job was cancelled', type='warning') + last_notified_status["status"] = 'cancelled' + last_notified_status["message"] = 'cancelled' + elif current_status == 'idle': + # Reset notification state when job is idle (ready for next job) + last_notified_status["status"] = None + last_notified_status["message"] = None def cancel_job(): """Cancel the current job.""" From 96fcb3af56586ca6051e57a389a31284a7a0b469 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 07:12:43 +0000 Subject: [PATCH 5/5] Add type annotations and input validation for robustness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type Annotation Fix: - Added explicit type hint for last_notified_status dict Type: dict[str, str | None] - Fixes Pyright errors about assigning strings to None type - Properly declares that dict values can be str or None Input Validation Fix: - Wrapped all int() conversions in try-except blocks - Added validation for positive values where appropriate - Provides clear error messages for invalid inputs Validation Details: - max_calib_samples: Must be positive integer - max_seq_length: Must be positive integer - w_bit: Must be integer (range validated in validate()) - group_size: Must be positive integer Error Messages: - ValueError with descriptive message showing invalid value - Catches both ValueError (bad string) and TypeError (None/invalid type) - Original exception chained for debugging Benefits: ✓ Prevents runtime errors from invalid user input ✓ UI allows custom values via editable combo boxes ✓ Clear error messages guide users to fix issues ✓ Type checker passes without errors ✓ Robust handling of edge cases (None, invalid strings, etc.) Example error: Input: max_seq_length="abc" Error: Invalid max_seq_length value 'abc': must be a positive integer --- src/msquant/app/pages/monitor.py | 2 +- src/msquant/core/quantizer/config.py | 35 +++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/msquant/app/pages/monitor.py b/src/msquant/app/pages/monitor.py index 1e137f6..bba918e 100644 --- a/src/msquant/app/pages/monitor.py +++ b/src/msquant/app/pages/monitor.py @@ -14,7 +14,7 @@ def create_monitor_page(job_service: JobService, gpu_monitor: GPUMonitor): available_gpus = {"list": []} # Track last notified status to prevent notification spam - last_notified_status = {"status": None, "message": None} + last_notified_status: dict[str, str | None] = {"status": None, "message": None} with ui.column().classes('w-full p-8 gap-4'): ui.label('Job Monitor').classes('text-4xl font-bold') diff --git a/src/msquant/core/quantizer/config.py b/src/msquant/core/quantizer/config.py index 4fa1252..0a18c1a 100644 --- a/src/msquant/core/quantizer/config.py +++ b/src/msquant/core/quantizer/config.py @@ -35,12 +35,35 @@ def __init__( self.calib_dataset = calib_dataset self.calib_config = calib_config self.calib_split = calib_split - self.max_calib_samples = int(max_calib_samples) - self.max_seq_length = int(max_seq_length) - - # AWQ - self.w_bit = int(w_bit) - self.group_size = int(group_size) + + # Convert numeric parameters with validation + try: + self.max_calib_samples = int(max_calib_samples) + if self.max_calib_samples <= 0: + raise ValueError("max_calib_samples must be positive") + except (ValueError, TypeError) as e: + raise ValueError(f"Invalid max_calib_samples value '{max_calib_samples}': must be a positive integer") from e + + try: + self.max_seq_length = int(max_seq_length) + if self.max_seq_length <= 0: + raise ValueError("max_seq_length must be positive") + except (ValueError, TypeError) as e: + raise ValueError(f"Invalid max_seq_length value '{max_seq_length}': must be a positive integer") from e + + # AWQ parameters + try: + self.w_bit = int(w_bit) + except (ValueError, TypeError) as e: + raise ValueError(f"Invalid w_bit value '{w_bit}': must be an integer") from e + + try: + self.group_size = int(group_size) + if self.group_size <= 0: + raise ValueError("group_size must be positive") + except (ValueError, TypeError) as e: + raise ValueError(f"Invalid group_size value '{group_size}': must be a positive integer") from e + self.zero_point = zero_point # NVFP4