Skip to content

feat: add comprehensive quantization framework with PTQ and QAT support - #816

Merged
ooples merged 22 commits into
masterfrom
feature/quantization-ptq-qat-278
Feb 4, 2026
Merged

ooples merged 22 commits into
masterfrom
feature/quantization-ptq-qat-278

Conversation

@ooples

@ooples ooples commented Feb 3, 2026

Copy link
Copy Markdown
Owner

Summary

Implements a complete model quantization framework supporting both Post-Training Quantization (PTQ) and Quantization-Aware Training (QAT), addressing issue #278.

PTQ Strategies

  • GPTQ: Hessian-based quantization with ActOrder optimization for minimal reconstruction error
  • AWQ: Activation-aware weight quantization that protects important weights based on activation magnitudes
  • SmoothQuant: Per-channel smoothing that transfers quantization difficulty from activations to weights (enables W8A8)
  • SpinQuant: Rotation-based quantization using Cayley parameterization for learned rotations
  • QuIP#: Extreme 2-bit quantization using Hadamard transforms and E8 lattice codebooks

Numeric Formats

  • INT8: Standard 8-bit integer quantization with symmetric/asymmetric modes
  • Float16: Half-precision floating point
  • FP8: E4M3 (training) and E5M2 (inference) formats
  • NF4: 4-bit NormalFloat optimal for normally distributed weights (used by QLoRA)
  • MXFP4: Microscaling FP4 with shared exponents per block (OCP standard)

QAT Support

  • QATTrainingHook: Applies fake quantization during training with Straight-Through Estimator
  • EfficientQATOptimizer: Block-wise quantization optimization
  • Configurable warmup epochs for gradual quantization introduction

Calibration Infrastructure

  • CalibrationHelper: Collects activation statistics via real forward passes when possible
  • Supports INeuralNetworkModel, INeuralNetwork, and generic IFullModel
  • Falls back to parameter-based estimation for models without forward pass support

Integration

  • Quantization wired into AiModelBuilder training pipeline
  • QuantizationInfo included in AiModelResult
  • Critical bug fix: Quantized model is now properly used instead of being discarded

Test plan

  • 62 integration tests covering all quantizers and strategies
  • Tests for calibration, QAT simulation, LoRA integration
  • Compression ratio and accuracy verification tests
  • Build passes on both .NET 10.0 and .NET Framework 4.7.1

Closes #278

🤖 Generated with Claude Code

…rt (#278)

Implements a complete model quantization framework supporting both Post-Training
Quantization (PTQ) and Quantization-Aware Training (QAT).

PTQ Strategies:
- GPTQ: Hessian-based quantization with ActOrder optimization
- AWQ: Activation-aware weight quantization for important weight protection
- SmoothQuant: Per-channel smoothing for W8A8 quantization
- SpinQuant: Rotation-based quantization using Cayley parameterization
- QuIP#: 2-bit quantization with Hadamard transforms and lattice codebooks

Numeric Formats:
- INT8: Standard 8-bit integer quantization
- Float16: Half-precision floating point
- FP8: E4M3 and E5M2 formats for training/inference
- NF4: 4-bit NormalFloat optimal for normally distributed weights (QLoRA)
- MXFP4: Microscaling FP4 with shared exponents (OCP standard)

QAT Support:
- QATTrainingHook: Fake quantization during training with STE
- EfficientQATOptimizer: Block-wise quantization optimization
- Warmup epochs for gradual quantization introduction

Calibration:
- CalibrationHelper: Collects activation statistics via real forward passes
- Supports INeuralNetworkModel, INeuralNetwork, and generic IFullModel
- Falls back to parameter-based estimation when forward passes unavailable

Integration:
- Quantization wired into AiModelBuilder training pipeline
- QuantizationInfo included in AiModelResult
- 62 comprehensive integration tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 3, 2026 02:00
@vercel

vercel Bot commented Feb 3, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
aidotnet-playground-api Ready Ready Preview, Comment Feb 4, 2026 8:22pm

@coderabbitai

coderabbitai Bot commented Feb 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Full quantization suite: many PTQ strategies (GPTQ, AWQ, SmoothQuant, SpinQuant, QuIPSharp), FP8/NF4/MXFP4 formats, INT8/Float16 support, per-group/per-channel granularity, presets and rich config (group size, target bit-width, activation bit-width, QAT options/warmup).
    • Calibration and activation-statistics collection; QAT support with memory-efficient QAT optimizer and training hooks.
    • Model results now include QuantizationInfo metadata (strategy, bit-width, compression, per-layer details).
  • Bug Fixes

    • Improved error handling to avoid training failures on non-fatal linkage issues.
  • Tests

    • Comprehensive integration tests covering PTQ/QAT flows, formats, calibration, presets, and edge cases.

Walkthrough

Integrates a full quantization subsystem: public config/enums, calibration/activation collectors, many quantizers/formats/strategies, QAT training hooks/optimizers, AiModelBuilder training integration with ApplyQuantizationIfConfigured, result-level QuantizationInfo, and comprehensive integration tests.

Changes

Cohort / File(s) Summary
Core integration
src/AiModelBuilder.cs, src/Models/Options/AiModelResultOptions.cs, src/Models/Results/AiModelResult.cs
Adds ApplyQuantizationIfConfigured and integrates post-training quantization into supervised training flow; surfaces QuantizationInfo in options/results; changes some exception handlers to catch (Exception).
Public config surface
src/Deployment/Configuration/QuantizationConfig.cs, src/Deployment/Optimization/Quantization/QuantizationConfiguration.cs
Expands public quantization config (strategy, granularity, groupSize, target/effective bit widths, activation/calibration, QAT options) and adds factory helpers and mapping to internal config.
Enums
src/Enums/QuantizationStrategy.cs, src/Enums/QuantizationGranularity.cs, src/Enums/QATMethod.cs
Adds documented enums for strategies, granularities, and QAT methods (new public API surface).
Calibration infra
src/Deployment/Optimization/Quantization/Calibration/ActivationStatistics.cs, src/Deployment/Optimization/Quantization/Calibration/CalibrationHelper.cs
New activation statistics and CalibrationHelper to collect per-layer/global stats from multiple data paths; produces calibration warnings and global stats.
Quantizer strategies
src/Deployment/Optimization/Quantization/Strategies/...
src/Deployment/Optimization/Quantization/Strategies/GPTQQuantizer.cs, .../AWQQuantizer.cs, .../SmoothQuantQuantizer.cs, .../SpinQuantQuantizer.cs, .../QuIPSharpQuantizer.cs
Adds multiple algorithmic quantizers implementing IQuantizer with Calibrate/Quantize flows, per-layer/group state, activation/Hessian handling, and metadata accessors.
Format quantizers
src/Deployment/Optimization/Quantization/Formats/...
src/Deployment/Optimization/Quantization/Formats/FP8Quantizer.cs, .../NF4Quantizer.cs, .../MXFP4Quantizer.cs
Adds FP8, NF4, MXFP4 format quantizers: encoding/decoding, calibration, block/codebook handling, quantize/dequantize logic and public helpers.
QAT training
src/Deployment/Optimization/Quantization/Training/QATTrainingHook.cs, src/Deployment/Optimization/Quantization/Training/EfficientQATOptimizer.cs
Adds QATTrainingHook (fake quantization, STE, QuantizationState) and EfficientQATOptimizer (block-wise QAT, progressive scheduling, block state).
Result metadata
src/Models/Results/QuantizationInfo.cs
Adds QuantizationInfo and LayerQuantizationInfo to report quantization metadata (strategy, bit widths, sizes, per-layer info, warnings).
Tests
tests/AiDotNet.Tests/IntegrationTests/Quantization/QuantizationIntegrationTests.cs
Adds extensive integration tests covering PTQ/QAT workflows, many quantizers/formats, calibration edge cases, QAT hooks/optimizers, and a lightweight test model.
Misc public surface
src/Enums/*, src/Models/*
Introduces many new public types/properties (QuantizationStrategy, QuantizationGranularity, QATMethod, QuantizationConfig/Configuration members, QuantizationInfo, QAT-related types) expanding the API surface.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(200,220,255,0.5)
    participant Builder as AiModelBuilder
    participant Trainer as Trainer
    participant Model as IFullModel
  end
  rect rgba(200,255,200,0.5)
    participant Calibrator as CalibrationHelper
    participant Quantizer as IQuantizer (selected)
  end
  rect rgba(255,230,200,0.5)
    participant Result as AiModelResult
  end

  Builder->>Trainer: Start training → produce BestSolution (model)
  Trainer->>Calibrator: CollectActivationStatistics(if configured)
  Calibrator-->>Trainer: ActivationStatistics
  Trainer->>Quantizer: Calibrate(model, data) / Quantize(model, config)
  alt quantization succeeds
    Quantizer-->>Trainer: QuantizedModel + QuantizationInfo
    Trainer->>Model: replace BestSolution with QuantizedModel
    Trainer->>Result: set QuantizationInfo in AiModelResultOptions
  else quantization fails
    Quantizer-->>Trainer: error/warnings
    Trainer->>Result: leave BestSolution unchanged, QuantizationInfo = null/None
  end
  Trainer->>Result: return AiModelResult (includes QuantizationInfo)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

"I hop through tensors, counting scales so bright,
I nibble at floats until bits fit tight,
I hum a tiny tune of compression and cheer,
I stash codebook crumbs and a crunchy carrot near,
Tiny paws, big hops — quantized dreams appear. 🐇"

🚥 Pre-merge checks | ✅ 3 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR implements comprehensive PTQ strategies (GPTQ, AWQ, SmoothQuant, SpinQuant, QuIP#), numeric formats (INT8, Float16, FP8, NF4, MXFP4), QAT support with training hooks and calibration infrastructure [#278]. However, critical architectural requirements are not fully met: mandatory PredictionModelBuilder integration is missing, IQuantizer interfaces lack clear inheritance pattern compliance, and INumericOperations usage across quantizers needs verification. Add PredictionModelBuilder integration with Configure methods for quantization features. Ensure all quantizers properly inherit from documented base classes. Verify INumericOperations usage throughout quantization code to meet mandatory project architectural requirements.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding a comprehensive quantization framework supporting both PTQ and QAT strategies.
Description check ✅ Passed The description provides relevant context about the quantization framework implementation, strategies, formats, and addresses the linked issue #278.
Out of Scope Changes check ✅ Passed The PR introduces extensive quantization framework code (new quantizers, calibration, QAT infrastructure, enums) that is within scope of issue #278. Minor error handling adjustments in AiModelBuilder and integration into the training pipeline are appropriate in-scope changes. Added QuantizationInfo to AiModelResult is expected for result payload construction.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/quantization-ptq-qat-278

Comment @coderabbitai help to get the list of available commands and usage tips.

Copilot AI 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.

Pull request overview

This PR implements a comprehensive quantization framework for model compression, supporting both Post-Training Quantization (PTQ) and Quantization-Aware Training (QAT). The implementation addresses issue #278 with advanced quantization strategies (GPTQ, AWQ, SmoothQuant, SpinQuant, QuIP#), multiple numeric formats (INT8, FP8, NF4, MXFP4), QAT support with multiple methods, and calibration infrastructure for collecting activation statistics.

Changes:

  • Adds 5 PTQ strategies (GPTQ, AWQ, SmoothQuant, SpinQuant, QuIP#) with Hessian-based optimization, activation-aware scaling, and learned rotations
  • Implements 4 numeric formats (INT8, FP8 E4M3/E5M2, NF4, MXFP4) for different compression scenarios
  • Integrates QAT support with fake quantization hooks and memory-efficient training methods
  • Adds calibration infrastructure using real forward passes when available, with fallback to parameter estimation
  • Wires quantization into AiModelBuilder training pipeline with proper result tracking

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 14 comments.

Show a summary per file
File Description
src/Models/Results/QuantizationInfo.cs Result class tracking quantization metrics (compression ratio, bit width, parameters)
src/Enums/QuantizationStrategy.cs Enum defining PTQ strategies with comprehensive documentation
src/Enums/QuantizationGranularity.cs Enum for quantization granularity levels (PerTensor, PerChannel, PerGroup)
src/Enums/QATMethod.cs Enum for QAT training methods (Standard, EfficientQAT, ZeroQAT, ParetoQ)
src/Deployment/Optimization/Quantization/Training/QATTrainingHook.cs Fake quantization application during training with STE
src/Deployment/Optimization/Quantization/Training/EfficientQATOptimizer.cs Memory-efficient QAT with block-wise quantization
src/Deployment/Optimization/Quantization/Strategies/GPTQQuantizer.cs GPTQ implementation with Hessian-based error compensation
src/Deployment/Optimization/Quantization/Strategies/AWQQuantizer.cs AWQ implementation with activation-aware weight protection
src/Deployment/Optimization/Quantization/Strategies/SmoothQuantQuantizer.cs SmoothQuant for W8A8 quantization with outlier smoothing
src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs SpinQuant with learned rotation matrices via Cayley parameterization
src/Deployment/Optimization/Quantization/Strategies/QuIPSharpQuantizer.cs QuIP# for extreme 2-bit quantization using Hadamard transforms
src/Deployment/Optimization/Quantization/Formats/NF4Quantizer.cs NF4 format implementation for QLoRA-style quantization
src/Deployment/Optimization/Quantization/Formats/MXFP4Quantizer.cs MXFP4 microscaling format with shared exponents
src/Deployment/Optimization/Quantization/Formats/FP8Quantizer.cs FP8 quantizer supporting E4M3 and E5M2 formats
src/Deployment/Optimization/Quantization/Calibration/CalibrationHelper.cs Collects activation statistics via forward passes
src/Deployment/Optimization/Quantization/Calibration/ActivationStatistics.cs Data structures for storing activation statistics
src/Deployment/Optimization/Quantization/QuantizationConfiguration.cs Internal configuration with extensive options and factory methods
src/Deployment/Configuration/QuantizationConfig.cs User-facing configuration with helper methods
src/AiModelBuilder.cs Integration of quantization into training pipeline

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs Outdated
Comment thread src/Deployment/Optimization/Quantization/Strategies/AWQQuantizer.cs
Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/Deployment/Optimization/Quantization/Formats/FP8Quantizer.cs Outdated
Comment thread src/Deployment/Optimization/Quantization/Strategies/GPTQQuantizer.cs Outdated
Comment thread src/Deployment/Optimization/Quantization/Formats/FP8Quantizer.cs Fixed
Comment thread src/Deployment/Optimization/Quantization/Formats/FP8Quantizer.cs Fixed
Comment thread src/Deployment/Optimization/Quantization/Formats/FP8Quantizer.cs Fixed
Comment thread src/AiModelBuilder.cs
Comment on lines +7239 to +7242
catch (Exception ex)
{
Console.WriteLine($"Warning: QAT simulation failed: {ex.Message}. Proceeding with standard PTQ.");
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

Copilot Autofix

AI 8 months ago

In general, to fix a generic catch clause you identify the specific, expected exception types that can reasonably occur in the protected block and catch those explicitly. You then either rethrow or do not catch unexpected exceptions, so programming errors and fatal conditions are not silently swallowed. This preserves robustness for expected issues while maintaining debuggability and correctness.

For this specific block (lines 7214–7234), the code is performing QAT “simulation” (instantiating a hook, calling OnEpochStart, GetParameters, ApplyFakeQuantization, and WithParameters). Expected problems here are likely configuration- or argument-related, such as ArgumentException, InvalidOperationException, or possibly NotSupportedException, if the model or configuration does not support the requested QAT method. We should catch these non-fatal, predictable exceptions and keep the existing behavior (log and fall back to standard PTQ). Severe or unexpected exceptions (e.g., OutOfMemoryException, NullReferenceException) should not be swallowed.

Concretely, in src/AiModelBuilder.cs, replace the single catch (Exception ex) at line 7231 with multiple, more specific catch blocks, such as catch (ArgumentException ex), catch (InvalidOperationException ex), and a final catch (NotSupportedException ex). Each block can reuse the existing log message. We should not introduce any new dependencies or imports, since these exception types are in System and already available. No changes are needed elsewhere.

Suggested changeset 1
src/AiModelBuilder.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/AiModelBuilder.cs b/src/AiModelBuilder.cs
--- a/src/AiModelBuilder.cs
+++ b/src/AiModelBuilder.cs
@@ -7228,10 +7228,18 @@
 
                 Console.WriteLine($"QAT simulation applied using {internalConfig.QATMethod} method");
             }
-            catch (Exception ex)
+            catch (ArgumentException ex)
             {
                 Console.WriteLine($"Warning: QAT simulation failed: {ex.Message}. Proceeding with standard PTQ.");
             }
+            catch (InvalidOperationException ex)
+            {
+                Console.WriteLine($"Warning: QAT simulation failed: {ex.Message}. Proceeding with standard PTQ.");
+            }
+            catch (NotSupportedException ex)
+            {
+                Console.WriteLine($"Warning: QAT simulation failed: {ex.Message}. Proceeding with standard PTQ.");
+            }
         }
 
         // Apply quantization - this returns a NEW model with quantized parameters
EOF
@@ -7228,10 +7228,18 @@

Console.WriteLine($"QAT simulation applied using {internalConfig.QATMethod} method");
}
catch (Exception ex)
catch (ArgumentException ex)
{
Console.WriteLine($"Warning: QAT simulation failed: {ex.Message}. Proceeding with standard PTQ.");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Warning: QAT simulation failed: {ex.Message}. Proceeding with standard PTQ.");
}
catch (NotSupportedException ex)
{
Console.WriteLine($"Warning: QAT simulation failed: {ex.Message}. Proceeding with standard PTQ.");
}
}

// Apply quantization - this returns a NEW model with quantized parameters
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread src/AiModelBuilder.cs Fixed
Comment thread src/AiModelBuilder.cs
Comment on lines +2381 to +2384
catch (Exception ex)
{
Console.WriteLine($"Warning: Quantization failed: {ex.Message}. Model will use original precision.");
}

Check notice

Code scanning / CodeQL

Generic catch clause Note

Generic catch clause.

Copilot Autofix

AI 8 months ago

General approach: replace the single broad catch (Exception) with more specific catch blocks for expected failures (e.g., ArgumentException, InvalidOperationException, potentially a domain‑specific quantization exception if available), and then add a final generic catch only if we still want a “last resort” safety net. This keeps robustness but avoids treating all failures as equivalent and lets us give more diagnostic information.

Best fix for this snippet without changing behavior:

  • Keep the surrounding try block intact.
  • Add explicit catch blocks for common runtime issues that might happen during quantization (ArgumentException, InvalidOperationException, and NotSupportedException are typical for configuration or operation‑not‑supported paths).
  • Adjust the existing generic catch to be last, and keep the same user‑visible behavior (warning and fallback), but expand the logged information slightly (e.g., include the exception type). This does not change functional behavior (still falls back to original precision) but helps diagnosis and makes it clear that we didn’t intend to swallow every exception type the same way.
  • No new imports are needed, as these exception types are in System.

Concretely, in src/AiModelBuilder.cs, around lines 2359–2363, replace the single catch (Exception ex) block with several specific catch blocks followed by a final generic catch. The rest of the method remains unchanged.


Suggested changeset 1
src/AiModelBuilder.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/AiModelBuilder.cs b/src/AiModelBuilder.cs
--- a/src/AiModelBuilder.cs
+++ b/src/AiModelBuilder.cs
@@ -2357,9 +2357,25 @@
                         $"{quantizationInfo.BitWidth}-bit, compression ratio: {quantizationInfo.CompressionRatio:F2}x");
                 }
             }
+            catch (ArgumentException ex)
+            {
+                // Likely caused by invalid quantization configuration or incompatible model parameters
+                Console.WriteLine($"Warning: Quantization failed due to invalid argument: {ex.Message}. Model will use original precision.");
+            }
+            catch (InvalidOperationException ex)
+            {
+                // Likely caused by an unsupported operation during quantization
+                Console.WriteLine($"Warning: Quantization failed due to invalid operation: {ex.Message}. Model will use original precision.");
+            }
+            catch (NotSupportedException ex)
+            {
+                // Likely caused by unsupported quantization mode or target
+                Console.WriteLine($"Warning: Quantization is not supported in this configuration: {ex.Message}. Model will use original precision.");
+            }
             catch (Exception ex)
             {
-                Console.WriteLine($"Warning: Quantization failed: {ex.Message}. Model will use original precision.");
+                // Last-resort handler to avoid training failure when quantization unexpectedly breaks
+                Console.WriteLine($"Warning: Quantization failed due to unexpected error ({ex.GetType().Name}): {ex.Message}. Model will use original precision.");
             }
         }
 
EOF
@@ -2357,9 +2357,25 @@
$"{quantizationInfo.BitWidth}-bit, compression ratio: {quantizationInfo.CompressionRatio:F2}x");
}
}
catch (ArgumentException ex)
{
// Likely caused by invalid quantization configuration or incompatible model parameters
Console.WriteLine($"Warning: Quantization failed due to invalid argument: {ex.Message}. Model will use original precision.");
}
catch (InvalidOperationException ex)
{
// Likely caused by an unsupported operation during quantization
Console.WriteLine($"Warning: Quantization failed due to invalid operation: {ex.Message}. Model will use original precision.");
}
catch (NotSupportedException ex)
{
// Likely caused by unsupported quantization mode or target
Console.WriteLine($"Warning: Quantization is not supported in this configuration: {ex.Message}. Model will use original precision.");
}
catch (Exception ex)
{
Console.WriteLine($"Warning: Quantization failed: {ex.Message}. Model will use original precision.");
// Last-resort handler to avoid training failure when quantization unexpectedly breaks
Console.WriteLine($"Warning: Quantization failed due to unexpected error ({ex.GetType().Name}): {ex.Message}. Model will use original precision.");
}
}

Copilot is powered by AI and may make mistakes. Always verify output.

@coderabbitai coderabbitai 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.

Actionable comments posted: 8

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Models/Results/AiModelResult.cs (1)

2340-2390: ⚠️ Potential issue | 🟡 Minor

Preserve QuantizationInfo across clone/deep-copy/deserialization.
Right now the new metadata is dropped when creating copies or rehydrating from JSON, so quantized models can appear unquantized in metadata.

🔧 Proposed fix
// WithParameters options object
             DeploymentConfiguration = DeploymentConfiguration,
             // JIT compilation is parameter-specific, don't copy
             InferenceOptimizationConfig = InferenceOptimizationConfig,
+            QuantizationInfo = QuantizationInfo,
             ReasoningConfig = ReasoningConfig,
// DeepCopy options object
             DeploymentConfiguration = DeploymentConfiguration,
             // JIT compilation is model-specific, don't copy
             InferenceOptimizationConfig = InferenceOptimizationConfig,
+            QuantizationInfo = QuantizationInfo,
             ReasoningConfig = ReasoningConfig,
// Deserialize property assignments
                 DeploymentConfiguration = deserializedObject.DeploymentConfiguration;
+                QuantizationInfo = deserializedObject.QuantizationInfo;

Also applies to: 4173-4221, 4366-4389

🤖 Fix all issues with AI agents
In `@src/AiModelBuilder.cs`:
- Around line 2356-2370: Calibration is currently sampled from the raw x only
when x is TInput[]/IEnumerable<TInput>, causing skipped or mis-scaled
calibration for preprocessed tensors; change the source of calibrationData to
use the preprocessed data (e.g., preprocessedX or XTrain) and ensure
ApplyQuantizationIfConfigured(optimizationResult.BestSolution,
_quantizationConfig, calibrationData) always receives samples by: if
preprocessedX is an array or IEnumerable<TInput> take up to 128 items, else
create a single-element IEnumerable<TInput> containing a representative
preprocessed batch (or the first batch extracted from a Matrix/Tensor) as a safe
fallback so calibration is never null and uses preprocessed inputs.

In
`@src/Deployment/Optimization/Quantization/Calibration/ActivationStatistics.cs`:
- Around line 59-63: Move the public generic class LayerActivationStats<T> into
its own file: create a new file named LayerActivationStats.cs (same namespace as
ActivationStatistics.cs), copy the full class declaration and its usings into
it, remove the class from the original ActivationStatistics.cs so only one
public class remains per file, and ensure any references to
LayerActivationStats<T> still resolve (adjust namespace/usings if needed); if
your project requires explicit file listing, add the new file to the project
file.
- Around line 109-156: The Update and UpdatePerChannelStats methods in
ActivationStatistics<T> currently convert elements with Convert.ToDouble and use
doubles
(MinValue/MaxValue/MaxAbsValue/Mean/Variance/SampleCount/PerChannelMaxAbs);
replace those conversions and double arithmetic with the generic
INumericOperations<T> API: obtain a local var ops =
INumericOperations<T>.Instance, use ops.Abs, ops.Compare,
ops.Add/Subtract/Divide/Multiply to update
MinValue/MaxValue/MaxAbsValue/Mean/Variance/SampleCount in T-space (or use
T-typed accumulators where appropriate) and only convert to double when
storing/exporting a double result; similarly update PerChannelMaxAbs by
comparing ops.Abs(values) per channel and convert to double only at the boundary
where PerChannelMaxAbs must be double. Ensure references to Update,
UpdatePerChannelStats, PerChannelMaxAbs, Mean, Variance, SampleCount, MinValue,
MaxValue, MaxAbsValue are updated to use the INumericOperations<T> methods
rather than Convert.ToDouble/hardcoded double math.

In `@src/Deployment/Optimization/Quantization/Formats/MXFP4Quantizer.cs`:
- Around line 172-213: QuantizeWithMXFP4 currently recomputes and overwrites
block scales even when Calibrate/ComputeBlockScales already populated
_scaleFactors; modify QuantizeWithMXFP4 to first check the IsCalibrated flag
and/or presence of a scale for $"block_{b}" and use that existing value instead
of recomputing it, only computing and storing a scale (using the current
maxAbs/maxMXFP4 logic) if no calibrated scale exists; ensure you do not
overwrite _scaleFactors["block_{b}"] when it already exists, and keep the rest
of the loop (FindNearestMXFP4Value, NumOps.FromDouble, _blockSize handling)
unchanged so quantization uses the calibrated scales when available.

In `@src/Deployment/Optimization/Quantization/Training/EfficientQATOptimizer.cs`:
- Around line 162-172: The loop in EfficientQATOptimizer (the block iterating
from start to end that uses gradOutput, result and NumOps.FromDouble) reads a
`scale` value but never applies it; either apply the block scale to the gradient
(e.g., multiply grad by the retrieved `scale` before calling NumOps.FromDouble
and assigning into `result`) so the "Scale gradient by block scale" comment is
implemented, or remove the unused `scale` variable and update the comment to
indicate STE/no-scaling is intentional; update the code around the loop in
EfficientQATOptimizer.cs accordingly.

In `@src/Deployment/Optimization/Quantization/Training/QATTrainingHook.cs`:
- Around line 205-223: The asymmetric branch in QATTrainingHook.cs computes
zeroPoint using scale that can be zero when maxVal == minVal; to fix, ensure
scale is clamped to a small positive value before computing zeroPoint (e.g.,
max((maxVal - minVal) / qMax, epsilon)) and then clamp zeroPoint to the valid
quant range [qMin, qMax] (use the existing qMin/qMax computed from bitWidth);
apply the same guard and clamping logic wherever activation stats are updated
(the same asymmetric computation used elsewhere) and reference
_config.UseSymmetricQuantization, scale, zeroPoint, bitWidth, qMin, qMax when
making the change.

In `@src/Models/Results/QuantizationInfo.cs`:
- Line 174: The Warnings property initializer uses the C# 12 collection
expression "[]", which fails on older target frameworks; update the initializer
for the property IReadOnlyList<string> Warnings in QuantizationInfo (property
name: Warnings) to use a compatible allocation such as new List<string>() or
Array.Empty<string>()/new string[0], or alternatively confirm and update the
project targets in the .csproj to require .NET 8+ so C# 12 is supported.
🟡 Minor comments (17)
src/Deployment/Optimization/Quantization/Strategies/SmoothQuantQuantizer.cs-81-84 (1)

81-84: ⚠️ Potential issue | 🟡 Minor

Mutating configuration object may cause unexpected behavior.

Similar to QuIPSharpQuantizer, this constructor modifies the _config object's Strategy property if it doesn't match. Consider creating a defensive copy instead.

tests/AiDotNet.Tests/IntegrationTests/Quantization/QuantizationIntegrationTests.cs-85-93 (1)

85-93: ⚠️ Potential issue | 🟡 Minor

Unused variable uniqueValues in test assertion.

The HashSet<double> uniqueValues is declared but never populated or used. The test appears incomplete - it should verify that quantized values have limited unique values (as the comment suggests) but the actual assertion only checks if values are on the quantization grid.

🔧 Proposed fix to remove unused variable
         // Verify parameters are quantized (should have limited unique values)
-        var uniqueValues = new HashSet<double>();
         for (int i = 0; i < quantizedParams.Length; i++)
         {
             // Scale factor determines the quantization step size
tests/AiDotNet.Tests/IntegrationTests/Quantization/QuantizationIntegrationTests.cs-1537-1541 (1)

1537-1541: ⚠️ Potential issue | 🟡 Minor

Assertion logic may be confusing and potentially incorrect.

The assertion at line 1537-1538 uses a double-negative logical condition that's hard to follow. The condition hook.IsQuantizationEnabled == false || duringWarmup[0] != weights[0] doesn't clearly express the intended behavior. During warmup, quantization should be disabled and values should pass through unchanged.

🔧 Proposed fix for clearer assertions
         // Assert
-        Assert.False(hook.IsQuantizationEnabled == false || duringWarmup[0] != weights[0],
-            "Quantization should be disabled during warmup (epoch 0 < 2)");
+        Assert.False(hook.IsQuantizationEnabled,
+            "Quantization should be disabled during warmup (epoch 0 < 2)");
+        Assert.Equal(weights[0], duringWarmup[0], 10);
         Assert.True(hook.IsQuantizationEnabled,
             "Quantization should be enabled after warmup");
src/Deployment/Optimization/Quantization/Strategies/QuIPSharpQuantizer.cs-80-83 (1)

80-83: ⚠️ Potential issue | 🟡 Minor

Mutating configuration object may cause unexpected behavior.

The constructor modifies the passed-in _config object's Strategy property if it doesn't match QuIPSharp. This mutation of a configuration object that may be shared could cause unexpected behavior. Consider creating a copy or throwing an exception instead.

🔧 Proposed fix to avoid config mutation
+        // Make a copy to avoid mutating shared config
+        if (config != null && config.Strategy != QuantizationStrategy.QuIPSharp)
+        {
+            _config = new QuantizationConfiguration
+            {
+                Mode = config.Mode,
+                Strategy = QuantizationStrategy.QuIPSharp,
+                TargetBitWidth = config.TargetBitWidth ?? 2,
+                Granularity = config.Granularity,
+                GroupSize = config.GroupSize,
+                UseSymmetricQuantization = config.UseSymmetricQuantization
+            };
+        }
+        else
+        {
             _config = config ?? new QuantizationConfiguration
             {
                 Mode = QuantizationMode.Int8,
                 Strategy = QuantizationStrategy.QuIPSharp,
                 TargetBitWidth = 2,
                 Granularity = QuantizationGranularity.PerGroup,
                 GroupSize = groupSize,
                 UseSymmetricQuantization = true
             };
-
-        if (_config.Strategy != QuantizationStrategy.QuIPSharp)
-        {
-            _config.Strategy = QuantizationStrategy.QuIPSharp;
         }
src/Deployment/Optimization/Quantization/Strategies/QuIPSharpQuantizer.cs-47-47 (1)

47-47: ⚠️ Potential issue | 🟡 Minor

Unused HadamardCache dictionary.

The HadamardCache static dictionary is declared but never populated or used. The current implementation uses the fast in-place Walsh-Hadamard transform instead. Remove this unused field to avoid confusion.

🧹 Proposed fix to remove unused field
     private readonly int _groupSize;
 
-    // Pre-computed Hadamard matrices for common sizes
-    private static readonly Dictionary<int, double[,]> HadamardCache = new();
-
     // E8 lattice codebook values for 2-bit quantization (4 levels)
     private static readonly double[] LatticeCodebook2Bit = { -1.0, -0.333, 0.333, 1.0 };
src/Deployment/Optimization/Quantization/Formats/NF4Quantizer.cs-108-152 (1)

108-152: ⚠️ Potential issue | 🟡 Minor

Calibration is effectively a no‑op right now.

Calibrate computes per‑block scales but the quantization path recomputes and overwrites them, and calibrationData is never used. Either use the calibration data to derive scales or reuse the cached scales when IsCalibrated to make calibration meaningful.

🔧 Make calibration scales take effect
-            // Compute block scale (absmax)
-            double maxAbs = 0;
-            for (int i = start; i < end; i++)
-            {
-                maxAbs = Math.Max(maxAbs, Math.Abs(Convert.ToDouble(parameters[i])));
-            }
-
-            double scale = maxAbs > 0 ? maxAbs : 1.0;
-            _scaleFactors[$"block_{b}"] = scale;
+            double scale;
+            if (IsCalibrated && _scaleFactors.TryGetValue($"block_{b}", out var cachedScale))
+            {
+                scale = cachedScale;
+            }
+            else
+            {
+                double maxAbs = 0;
+                for (int i = start; i < end; i++)
+                {
+                    maxAbs = Math.Max(maxAbs, Math.Abs(Convert.ToDouble(parameters[i])));
+                }
+
+                scale = maxAbs > 0 ? maxAbs : 1.0;
+                _scaleFactors[$"block_{b}"] = scale;
+            }

Also applies to: 168-176

src/Deployment/Optimization/Quantization/Strategies/AWQQuantizer.cs-138-239 (1)

138-239: ⚠️ Potential issue | 🟡 Minor

Clear per‑run state to avoid stale scales/zero‑points.

_scaleFactors and _zeroPoints are never cleared, so repeated quantizations can include stale values and corrupt the “global” averages or per‑group lookup. Clear these at the start of quantization to keep state consistent.

🧹 Proposed fix
 private Vector<T> QuantizeWithAWQ(Vector<T> parameters, QuantizationConfiguration config)
 {
+    _scaleFactors.Clear();
+    _zeroPoints.Clear();
+
     int n = parameters.Length;
     int groupSize = config.Granularity == QuantizationGranularity.PerGroup ? config.GroupSize : n;
src/Deployment/Optimization/Quantization/QuantizationConfiguration.cs-123-124 (1)

123-124: ⚠️ Potential issue | 🟡 Minor

Deprecation warning is helpful, but BitWidth still returns DefaultBitWidth, not EffectiveBitWidth.

The [Obsolete] message tells users to use EffectiveBitWidth, but BitWidth returns DefaultBitWidth which ignores TargetBitWidth. This could cause confusion. Consider either:

  1. Having BitWidth return EffectiveBitWidth for backward compatibility, or
  2. Making it clearer that the values differ
src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs-71-94 (1)

71-94: ⚠️ Potential issue | 🟡 Minor

Constructor silently mutates the passed configuration object.

Same issue as GPTQQuantizer: if a non-null config with different strategy is passed, the constructor mutates it at line 88. Consider throwing instead or creating a defensive copy.

src/Deployment/Optimization/Quantization/Formats/MXFP4Quantizer.cs-101-110 (1)

101-110: ⚠️ Potential issue | 🟡 Minor

Quantize ignores the config parameter.

The method signature accepts a QuantizationConfiguration config parameter but uses _config (from constructor) and _blockSize throughout. Either use the passed config or remove the parameter from the interface if it's not needed.

src/Deployment/Optimization/Quantization/Training/EfficientQATOptimizer.cs-55-61 (1)

55-61: ⚠️ Potential issue | 🟡 Minor

Starting bit width logic may produce unexpected values.

Line 59 sets _currentBitWidth = Math.Min(32, _config.EffectiveBitWidth * 2). If EffectiveBitWidth is 4, this starts at 8, but UpdateProgressiveQuantization (line 267) uses Math.Min(16, ...) as the start. This inconsistency means the constructor's initial value may never be used if QATMethod == EfficientQAT.

src/Deployment/Optimization/Quantization/Training/EfficientQATOptimizer.cs-99-102 (1)

99-102: ⚠️ Potential issue | 🟡 Minor

Integer overflow risk with bit shift operations.

Same issue as GPTQQuantizer: 1 << (effectiveBitWidth - 1) overflows for effectiveBitWidth >= 32. Use 1L << for safety.

🛠️ Suggested fix
-double qMin = _config.UseSymmetricQuantization ? -(1 << (effectiveBitWidth - 1)) : 0;
-double qMax = _config.UseSymmetricQuantization ? (1 << (effectiveBitWidth - 1)) - 1 : (1 << effectiveBitWidth) - 1;
+double qMin = _config.UseSymmetricQuantization ? -(1L << (effectiveBitWidth - 1)) : 0;
+double qMax = _config.UseSymmetricQuantization ? (1L << (effectiveBitWidth - 1)) - 1 : (1L << effectiveBitWidth) - 1;
src/Deployment/Optimization/Quantization/Strategies/GPTQQuantizer.cs-151-153 (1)

151-153: ⚠️ Potential issue | 🟡 Minor

Integer overflow risk for high bit widths.

The expressions 1 << (bitWidth - 1) and 1 << bitWidth will overflow when bitWidth >= 32. Although typical use is 4-8 bits, the API accepts any EffectiveBitWidth. Consider using 1L << ... or clamping bit width.

🛠️ Suggested fix
-double qMin = config.UseSymmetricQuantization ? -(1 << (bitWidth - 1)) : 0;
-double qMax = config.UseSymmetricQuantization ? (1 << (bitWidth - 1)) - 1 : (1 << bitWidth) - 1;
+double qMin = config.UseSymmetricQuantization ? -(1L << (bitWidth - 1)) : 0;
+double qMax = config.UseSymmetricQuantization ? (1L << (bitWidth - 1)) - 1 : (1L << bitWidth) - 1;
src/Deployment/Optimization/Quantization/Strategies/GPTQQuantizer.cs-70-78 (1)

70-78: ⚠️ Potential issue | 🟡 Minor

Constructor silently mutates the passed configuration object.

If a non-null config is provided with a different Strategy, the constructor modifies _config.Strategy directly. Since _config holds a reference to the caller's object, this mutates state outside the class unexpectedly.

Consider creating a defensive copy or throwing if the strategy doesn't match:

🛠️ Suggested fix
 public GPTQQuantizer(QuantizationConfiguration? config = null)
 {
-    _config = config ?? QuantizationConfiguration.ForGPTQ();
-
-    if (_config.Strategy != QuantizationStrategy.GPTQ)
-    {
-        _config.Strategy = QuantizationStrategy.GPTQ;
-    }
+    if (config != null && config.Strategy != QuantizationStrategy.GPTQ)
+    {
+        throw new ArgumentException(
+            $"Configuration must use GPTQ strategy, but was {config.Strategy}", nameof(config));
+    }
+    _config = config ?? QuantizationConfiguration.ForGPTQ();
 }
src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs-475-504 (1)

475-504: ⚠️ Potential issue | 🟡 Minor

QuantizeSymmetric overwrites the global scale factor.

Both ComputeScaleFactors (called during Calibrate) and QuantizeSymmetric (called during Quantize) write to _scaleFactors["global"]. The calibration scale is overwritten during quantization, which may not be intentional.

src/Deployment/Optimization/Quantization/Strategies/GPTQQuantizer.cs-252-257 (1)

252-257: ⚠️ Potential issue | 🟡 Minor

Global scale averaging may produce misleading values.

The global scale is computed as the average of all group scales, but _scaleFactors also contains non-group keys (like "global" itself after the first run). This could skew the average on subsequent quantizations if the cache isn't cleared.

🛠️ Suggested fix
 // Store global scale (average)
-if (_scaleFactors.Count > 0)
+var groupScales = _scaleFactors.Where(kv => kv.Key.StartsWith("group_")).Select(kv => kv.Value).ToList();
+if (groupScales.Count > 0)
 {
-    _scaleFactors["global"] = _scaleFactors.Values.Average();
+    _scaleFactors["global"] = groupScales.Average();
     _zeroPoints["global"] = 0;
 }
src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs-174-197 (1)

174-197: ⚠️ Potential issue | 🟡 Minor

Optimization loop has numerical stability issues.

  1. The Cayley parameter update at lines 180-193 enforces skew-symmetry by setting cayleyParam[j, i] = -cayleyParam[i, j] only when i < j, but gradient descent updates both [i,j] and [j,i] independently before this correction, causing inconsistent states during the inner loop.
  2. Setting diagonal to zero at line 192 happens inside the j loop, overwriting values multiple times unnecessarily.
🛠️ Suggested restructure
 // Update Cayley parameter
 for (int i = 0; i < rotationSize; i++)
 {
-    for (int j = 0; j < rotationSize; j++)
+    for (int j = i + 1; j < rotationSize; j++) // Only update upper triangle
     {
-        cayleyParam[i, j] -= _learningRate * gradient[i, j];
-
-        // Enforce skew-symmetry: A[i,j] = -A[j,i]
-        if (i < j)
-        {
-            cayleyParam[j, i] = -cayleyParam[i, j];
-        }
+        double update = _learningRate * (gradient[i, j] - gradient[j, i]) / 2;
+        cayleyParam[i, j] -= update;
+        cayleyParam[j, i] = -cayleyParam[i, j]; // Enforce skew-symmetry
     }
-    cayleyParam[i, i] = 0; // Diagonal must be zero
+    // Diagonal is already zero for skew-symmetric matrices
 }
🧹 Nitpick comments (17)
tests/AiDotNet.Tests/IntegrationTests/Quantization/QuantizationIntegrationTests.cs (2)

23-23: Unused constant Tolerance.

The constant Tolerance is declared but never used in any of the test assertions. Consider removing it or using it in tests that compare floating-point values.


45-57: Consider returning IEnumerable<double[]> eagerly to avoid repeated enumeration.

The CreateCalibrationData method uses yield return, which is lazy. Several tests call .ToList() on the result, which is correct. However, since most consumers materialize the collection anyway, consider returning List<double[]> directly to avoid potential multiple-enumeration issues if a caller forgets .ToList().

src/Deployment/Optimization/Quantization/Formats/FP8Quantizer.cs (2)

191-192: NaN handling returns 0, which may silently mask data issues.

When the input value is NaN, the method returns 0 without any warning or logging. This could mask data corruption or numerical instability issues in the model weights. Consider tracking or logging NaN occurrences during quantization.


430-446: Consider moving FP8Format enum to a separate file.

Per PR objectives stating "one class per file and namespaces matching folders," the FP8Format enum should ideally be in its own file. However, since it's tightly coupled with the quantizer and relatively small, this is a minor organizational concern.

src/Deployment/Optimization/Quantization/Calibration/CalibrationHelper.cs (4)

135-139: Silent catch blocks may hide legitimate errors.

The empty catch blocks that silently continue to the next sample could mask important errors like out-of-memory conditions or type mismatches. Consider at least logging or counting these failures for diagnostics.

🔧 Proposed fix to track failures
+            int failedSamples = 0;
             foreach (var sample in samples)
             {
                 try
                 {
                     // ... existing code ...
                 }
                 catch
                 {
-                    // Skip samples that fail - continue with remaining
-                    continue;
+                    // Skip samples that fail - continue with remaining
+                    failedSamples++;
                 }
             }
+            
+            if (failedSamples > 0 && stats.SampleCount == 0)
+            {
+                throw new InvalidOperationException($"All {failedSamples} calibration samples failed processing");
+            }

372-383: CanRunPredictions method provides no real validation.

The method always returns true for non-null models and catches any exception to return false, but it doesn't actually test if predictions can run. This provides a false sense of safety. Consider removing the try-catch or actually attempting a prediction.

🔧 Proposed simplification
     private bool CanRunPredictions(IFullModel<T, TInput, TOutput> model)
     {
-        // Check if model has been trained and can make predictions
-        try
-        {
-            return model != null;
-        }
-        catch
-        {
-            return false;
-        }
+        return model != null;
     }

410-426: Reflection-based conversion may have performance implications.

The fallback to reflection-based ToArray() method invocation could be slow when processing many calibration samples. This is acceptable as a last-resort fallback, but consider documenting this performance characteristic.


456-467: Potential boxing overhead for scalar output.

The pattern if (output is T scalar) will always match when TOutput is T, which could cause unintended behavior. For value types, this also involves boxing. Consider checking this case more explicitly or reordering the type checks.

src/Deployment/Optimization/Quantization/Strategies/SmoothQuantQuantizer.cs (2)

259-262: Per-channel estimation uses heuristic that may not match actual layer structure.

The per-channel quantization estimates the number of channels as sqrt(n), which assumes a square-ish parameter distribution. This heuristic may not accurately reflect the actual layer structure, potentially leading to suboptimal quantization. Consider accepting layer structure information from the model metadata if available.


306-309: Computing average of all scale factors may be misleading.

Setting the global scale factor as the average of all channel/group scale factors may not be meaningful and could be confusing when retrieved via GetScaleFactor("global"). Consider using max, median, or documenting this behavior clearly.

src/Deployment/Optimization/Quantization/Formats/NF4Quantizer.cs (1)

97-106: Validate or honor the config argument to avoid silent mismatches.

Quantize ignores the passed configuration, so callers can supply incompatible settings without any signal. Consider validating bit‑width and group size to prevent surprising results.

🔧 Possible guard
 public IFullModel<T, TInput, TOutput> Quantize(IFullModel<T, TInput, TOutput> model, QuantizationConfiguration config)
 {
     if (model == null) throw new ArgumentNullException(nameof(model));
+    if (config.TargetBitWidth != 4)
+        throw new ArgumentException("NF4 requires TargetBitWidth = 4.", nameof(config));
+    if (config.Granularity == QuantizationGranularity.PerGroup && config.GroupSize != _blockSize)
+        throw new ArgumentException("NF4 block size must match config.GroupSize.", nameof(config));
 
     var parameters = model.GetParameters();
     var quantizedParams = QuantizeWithNF4(parameters);
src/Deployment/Optimization/Quantization/Strategies/AWQQuantizer.cs (1)

251-331: Preserve activation scales even when length < parameter count.

When _activationScales["global"] is shorter than n, the current logic falls back to uniform scales, effectively disabling AWQ. Consider expanding/tiling the activation scales instead of dropping them entirely.

♻️ Possible expansion strategy
 private double[] GetActivationScales(int n)
 {
-    if (_activationScales.TryGetValue("global", out var scales) && scales.Length >= n)
-    {
-        return scales;
-    }
+    if (_activationScales.TryGetValue("global", out var scales) && scales.Length > 0)
+    {
+        if (scales.Length >= n) return scales;
+        var expanded = new double[n];
+        for (int i = 0; i < n; i++)
+        {
+            expanded[i] = scales[i % scales.Length];
+        }
+        return expanded;
+    }
src/Deployment/Optimization/Quantization/Strategies/GPTQQuantizer.cs (2)

387-404: Condition at lines 391-392 is always true and redundant.

The modulo calculation i - (i / hessianDiag.Length * hessianDiag.Length) equals i % hessianDiag.Length, which is always < hessianDiag.Length. The check is tautological and the subsequent check at line 397 is also always true since iLocal and jLocal are computed via modulo.

♻️ Simplified version
 private double GetHessianCrossElement(int i, int j, double[] hessianDiag)
 {
-    // Simplified: use geometric mean of diagonals for off-diagonal elements
-    // Full GPTQ would use actual H^-1 elements from Cholesky decomposition
-    if (i - (i / hessianDiag.Length * hessianDiag.Length) < hessianDiag.Length &&
-        j - (j / hessianDiag.Length * hessianDiag.Length) < hessianDiag.Length)
-    {
-        int iLocal = i % hessianDiag.Length;
-        int jLocal = j % hessianDiag.Length;
-
-        if (iLocal < hessianDiag.Length && jLocal < hessianDiag.Length)
-        {
-            return Math.Sqrt(hessianDiag[iLocal] * hessianDiag[jLocal]) * 0.1;
-        }
-    }
-
-    return _config.GPTQDampingFactor;
+    // Simplified: use geometric mean of diagonals for off-diagonal elements
+    // Full GPTQ would use actual H^-1 elements from Cholesky decomposition
+    int iLocal = i % hessianDiag.Length;
+    int jLocal = j % hessianDiag.Length;
+    return Math.Sqrt(hessianDiag[iLocal] * hessianDiag[jLocal]) * 0.1;
 }

406-413: Nested class HessianInfo could use required or constructor for strict initialization.

Per PR objectives, strict property initialization is required (no default!). The Diagonal property is nullable but Size defaults to 0, which may be unintentional. Consider using a constructor or required modifier.

src/Deployment/Optimization/Quantization/Training/EfficientQATOptimizer.cs (1)

335-359: BlockQuantizationState class should be in its own file.

Per PR objectives: "one class per file and namespaces matching folders." Move BlockQuantizationState to a separate file.

src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs (1)

386-394: Singular matrix handling returns identity without warning.

When the matrix is singular (pivot < 1e-10), the method silently returns an identity matrix. This could mask numerical issues during rotation learning. Consider logging a warning or throwing.

src/Deployment/Optimization/Quantization/Formats/MXFP4Quantizer.cs (1)

245-266: EncodeToMXFP4 and DecodeFromMXFP4 are public but appear to be internal helpers.

These static methods are exposed publicly but seem intended for internal use. If they're part of the public API for serialization/deserialization, consider documenting this use case. Otherwise, mark them internal or private.

Comment thread src/AiModelBuilder.cs Outdated
Comment thread src/Deployment/Optimization/Quantization/Formats/MXFP4Quantizer.cs
Comment thread src/Models/Results/QuantizationInfo.cs Outdated
franklinic and others added 2 commits February 3, 2026 13:53
- EfficientQATOptimizer: make _totalEpochs readonly, use scale in gradient computation
- CalibrationHelper: remove unused avgMaxAbs, use explicit Exception catches
- FP8Quantizer: remove unused fp8MinSubnormal, use ternary for format selection
- MXFP4Quantizer: reuse calibrated scale factors if available
- GPTQQuantizer: use ternary for Hessian diagonal assignment
- SpinQuantQuantizer: actually use calibration data for activation statistics
- QATTrainingHook: guard zero-point computation after scale clamping
- QuantizationInfo: replace [] with Array.Empty for net471 compatibility
- AiModelBuilder: use explicit Exception catches for generic catch clauses

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Use XTrain (preprocessed) instead of raw x for calibration data
- Handle non-enumerable datasets by wrapping single items
- Estimate parameter size based on actual type T instead of assuming double

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- SmoothQuantQuantizer: use log-space computation for numerical stability
- QuantizationConfiguration: add configurable AWQScaleSearchOptions
- AWQQuantizer: use configurable scale search options instead of hardcoded values

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 3, 2026 19:02
IEEE FP8 E4M3 can represent NaN but we map to 0 for safer ML inference

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@coderabbitai coderabbitai 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.

Actionable comments posted: 14

🤖 Fix all issues with AI agents
In `@src/AiModelBuilder.cs`:
- Around line 7207-7215: The size calculations use int multiplication and can
overflow for large models; change the multiplications to use long by casting
operands before multiplying (e.g., when computing originalSizeBytes use
(long)parameters.Length * bytesPerParameter instead of parameters.Length *
bytesPerParameter) and likewise cast before computing quantized size (e.g.,
(long)quantizedParameters.Length * EffectiveBitWidth). Update the expressions in
the block around model.GetParameters() and the similar block that computes
quantizedParameters.Length * EffectiveBitWidth so the product is evaluated in
64-bit arithmetic.
- Around line 7190-7205: The MinMax/Dynamic branch currently silently defaults
to Int8 for any QuantizationMode not Int8/Float16, which causes FP8/NF4/MXFP4
configs to be mis-applied; update the branch in AiModelBuilder.cs where
internalConfig.Mode is checked so that FP8/NF4/MXFP4 modes are explicitly
handled (instantiate the correct quantizer types if available, e.g.,
FP8Quantizer/NF4Quantizer/MXFP4Quantizer in
Deployment.Optimization.Quantization) or throw a clear exception listing
unsupported modes, instead of returning new
Deployment.Optimization.Quantization.Int8Quantizer<T, TInput, TOutput>(); ensure
you reference internalConfig.Mode, QuantizationMode, and the quantizer classes
(e.g., Int8Quantizer, Float16Quantizer, FP8Quantizer) when making the change so
the code fails fast for unknown/unsupported modes.

In `@src/Deployment/Optimization/Quantization/Calibration/CalibrationHelper.cs`:
- Around line 151-184: The code sets the model into inference mode with
model.SetTrainingMode(false) and never restores the prior training mode; capture
the original mode before changing it, wrap the sampling loop in try/finally, and
call model.SetTrainingMode(originalMode) in the finally block so the model's
training state is always restored after the loop (reference
model.SetTrainingMode, samples, and the sampling loop that updates
stats.LayerStats and stats.SampleCount).
- Around line 395-401: The code uses C# 12 collection expressions for tensor
shapes in CalibrationHelper (the Tensor<T> constructors in the method that
handles Matrix<T> and T[] samples); replace the `[matrix.Rows, matrix.Columns]`
and `[1, array.Length]` expressions with backward-compatible array initializers
(e.g., new int[] { ... } or new[] { ... }) so the project compiles under net471,
or alternatively set LangVersion to 12.0 in the project file if you intend to
keep the C#12 syntax; update the two Tensor<T> calls accordingly.

In `@src/Deployment/Optimization/Quantization/Formats/MXFP4Quantizer.cs`:
- Around line 101-110: The Quantize method currently ignores the incoming
QuantizationConfiguration; update IFullModel<T, TInput, TOutput>.Quantize
implementation to respect the config by validating it and passing its settings
into the quantization routine instead of using constructor defaults: check for
null config and validate fields (e.g., group size, bit width), throw
ArgumentException for unsupported values, then call QuantizeWithMXFP4 (or an
overload) with the config-derived parameters and return
model.WithParameters(quantizedParams); reference the Quantize method and the
QuantizeWithMXFP4 helper to locate changes.
- Around line 175-223: The QuantizeWithMXFP4 method currently reuses
_scaleFactors entries keyed only by block index (block_{b}), which can leak
scales across different models; update the logic so _scaleFactors cannot be
reused across models—either incorporate a model-specific prefix into the
blockKey (e.g., include a modelId or weightBufferId when constructing blockKey)
or ensure the quantizer clears or re-initializes _scaleFactors when a new model
is loaded; modify the code paths that set or read _scaleFactors in
QuantizeWithMXFP4 to use the new model-scoped key or to check a modelVersion
flag so stale scales are never applied to a different model.

In `@src/Deployment/Optimization/Quantization/QuantizationConfiguration.cs`:
- Line 71: Granularity defaults to PerChannel while UsePerChannelQuantization
defaults to false causing inconsistent/overlapping behavior; reconcile by
deprecating UsePerChannelQuantization and mapping it to Granularity (or choose a
single source of truth): mark UsePerChannelQuantization as obsolete, implement
its getter/setter to return/set Granularity ==
QuantizationGranularity.PerChannel so both stay consistent, and ensure the
default value is driven from Granularity (keep Granularity default PerChannel
and remove independent defaulting for UsePerChannelQuantization) or
alternatively remove UsePerChannelQuantization entirely and update callers to
use Granularity.
- Around line 251-260: The AWQProtectionPercentage property is ambiguous about
whether it uses a 0–1 ratio or 0–100 percentage; decide on a single
representation, then update the property and docs accordingly: either (A) keep a
percentage name and set default to 1.0 with doc stating "1.0 = 1%" and validate
range 0–100, or (B) switch to a ratio by renaming AWQProtectionPercentage to
AWQProtectionRatio (or change default to 0.01) and update the XML remarks to
"0.0–1.0, default 0.01 = 1%"; update all usages of AWQProtectionPercentage/
AWQProtectionRatio, adjust unit wording in the summary/remarks, and add a guard
(e.g., range check) in any setter/validation logic to enforce the chosen scale.

In `@src/Deployment/Optimization/Quantization/Strategies/AWQQuantizer.cs`:
- Around line 317-331: GetActivationScales currently drops calibration when the
stored scales array is shorter than the parameter count (n); update
GetActivationScales to map/expand shorter calibration arrays instead of
returning a uniform default: when _activationScales contains an entry (e.g.,
"global") with scales.Length < n, produce a result of length n by
repeating/tiling or mapping per-channel/per-layer (e.g., use modulo indexing or
a per-group mapping strategy) so calibrated values are applied across the full
parameter vector rather than replaced by defaultScales; keep the existing return
when no scales entry exists but avoid discarding existing calibration when
scales.Length > 0 and < n.

In `@src/Deployment/Optimization/Quantization/Strategies/GPTQQuantizer.cs`:
- Around line 164-234: The Hessian diagonal returned by
GetHessianDiagonal(groupStart, actualGroupSize) is indexed as if group indices
are contiguous, but when ActOrder is enabled the processing order[] permutes
indices so hessianDiag is misaligned; fix by fetching or mapping the Hessian per
the ordered index (use idx = order[i]) before using it in the OBS update: either
call GetHessianDiagonal for each ordered index or build hessianDiagMapped[k] =
originalHessian[order[groupStart+k]] and then use hDiag = hessianDiagMapped[i -
groupStart] where hDiag is currently read; update references in the quantization
loop (symbols: GetHessianDiagonal, order, hessianDiag, idx, groupStart/groupEnd)
so error compensation uses the correct curvature entries.

In `@src/Deployment/Optimization/Quantization/Strategies/SmoothQuantQuantizer.cs`:
- Around line 239-244: In SmoothQuantQuantizer, after computing the asymmetric
zeroPoint (the branches that set scale and zeroPoint using bitWidth, min/max and
config.MinScaleFactor), clamp zeroPoint to the quant range [qMin, qMax] (e.g.
zeroPoint = Math.Max(qMin, Math.Min(qMax, zeroPoint))) so it cannot fall outside
the representable quantized range; apply this clamp in the same pattern for the
other asymmetric zeroPoint computations referenced (the blocks around the other
occurrences).

In `@src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs`:
- Around line 103-117: The returned model currently contains
rotated-and-quantized weights but never applies the inverse rotation (R^T),
which changes semantics; after QuantizeSymmetric you must undo the learned
rotation before returning or ensure inference applies R^T. Fix by computing the
inverse/transpose of the rotation produced by LearnRotationMatrix (use
rotation.Transpose() or an ApplyInverseRotation(parameters, rotation) helper),
apply that inverse to quantizedRotated to restore weights to the original space,
and then return model.WithParameters(restoredParams); alternatively, persist
_rotationMatrices["global"] and wrap affected layers so their forward passes
multiply by R^T, but do not return rotated weights directly from
QuantizeSymmetric without restoring them.

In `@src/Deployment/Optimization/Quantization/Training/EfficientQATOptimizer.cs`:
- Around line 281-332: The asymmetric path currently computes blockZeroPoints[b]
before clamping blockScales[b], which can divide by zero when maxVal==minVal;
fix InitializeBlockState by computing the raw scale first (e.g., scaleRaw =
(maxVal - minVal) / ((1 << effectiveBitWidth) - 1)), clamp it with
blockScales[b] = Math.Max(scaleRaw, _config.MinScaleFactor), then compute
blockZeroPoints[b] = (int)Math.Round(-minVal / blockScales[b]) and finally clamp
that zero point into the valid quant range [0, qMax]; keep the symmetric branch
unchanged and ensure you reference blockScales[b] and blockZeroPoints[b] when
assigning.

In `@src/Deployment/Optimization/Quantization/Training/QATTrainingHook.cs`:
- Around line 114-120: The activation state is initialized with default (weight)
bit-width then overwritten, so QuantMin/QuantMax/Scale are computed using the
wrong bit-width; change InitializeLayerState to accept a bit-width parameter and
call it with _config.ActivationBitWidth when creating activation state
(activationKey path) so the returned LayerState.BitWidth,
QuantMin/QuantMax/Scale are computed from the activation bit width; update all
other activation-initialization sites (the same pattern around the block
referenced at lines ~190-243) to pass the activation bit width as well and
remove the post-assignment of BitWidth.
🧹 Nitpick comments (2)
src/Deployment/Optimization/Quantization/Formats/MXFP4Quantizer.cs (1)

147-170: Persist a global scale for metadata consumers.
GetScaleFactor will usually return 1.0 because only block_{b} keys are stored. Consider saving an aggregate (global) scale during computation so metadata isn’t misleading.

♻️ Suggested addition
 private void ComputeBlockScales(Vector<T> parameters)
 {
     int n = parameters.Length;
     int numBlocks = (n + _blockSize - 1) / _blockSize;
+    double scaleSum = 0;

     for (int b = 0; b < numBlocks; b++)
     {
         int start = b * _blockSize;
         int end = Math.Min(start + _blockSize, n);
         ...
         _scaleFactors[$"block_{b}"] = scale;
+        scaleSum += scale;
     }
+
+    if (numBlocks > 0)
+    {
+        _scaleFactors["global"] = scaleSum / numBlocks;
+    }
 }
src/Deployment/Optimization/Quantization/QuantizationConfiguration.cs (1)

262-270: Consider using IReadOnlyList<double> or defensive copying for AWQScaleSearchOptions.

Exposing a mutable double[] allows callers to accidentally mutate the configuration's internal state. For a configuration object, this could lead to subtle bugs.

♻️ Proposed refactor to use immutable collection
-    public double[] AWQScaleSearchOptions { get; set; } = [0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0];
+    public IReadOnlyList<double> AWQScaleSearchOptions { get; set; } = [0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0];

Comment thread src/AiModelBuilder.cs
Comment thread src/Deployment/Optimization/Quantization/Calibration/CalibrationHelper.cs Outdated
Comment thread src/Deployment/Optimization/Quantization/Formats/MXFP4Quantizer.cs
Comment thread src/Deployment/Optimization/Quantization/Formats/MXFP4Quantizer.cs Outdated
Comment thread src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs Outdated
- Fix int overflow in quantizedSizeBytes calculation (AiModelBuilder.cs)
- Fix floating-point equality checks using epsilon comparison (FP8Quantizer.cs)
- Improve NF4 codebook documentation with QLoRA reference (NF4Quantizer.cs)
- Use adaptive epsilon for gradient computation (SpinQuantQuantizer.cs)
- Add calibration failure tracking with warnings (CalibrationHelper.cs)
- Add CalibrationWarnings property to ActivationStatistics

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix more floating-point equality checks in FP8Quantizer (ToE4M3, ToE5M2)
- Add detailed QAT simulation documentation explaining it's post-training only
- Improve exception logging to include exception type and inner exception
- Clarify that true QAT requires training loop integration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 3, 2026 20:46
- Add comprehensive documentation for ConvertToTensor supported types
- Add return type validation for reflection-based ToArray conversion
- Simplify and document GetHessianCrossElement index mapping logic
- Add zero-length array guard in Hessian cross-element calculation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/Deployment/Optimization/Quantization/Calibration/CalibrationHelper.cs`:
- Around line 498-507: The assignability check in the calibration helper is
inverted: instead of checking returnType.IsAssignableFrom(typeof(T[])), change
the condition to check whether typeof(T[]).IsAssignableFrom(returnType) (or
simply compare returnType == typeof(T[]) ||
typeof(T[]).IsAssignableFrom(returnType)) so you correctly detect when the
method's ReturnType can be assigned to T[] before invoking toArrayMethod; keep
the rest of the logic that invokes toArrayMethod and constructs the Tensor<T>
(using toArrayMethod, returnType, Tensor<T>, Vector<T>) unchanged.

In `@src/Deployment/Optimization/Quantization/Training/EfficientQATOptimizer.cs`:
- Around line 371-377: The property initializers BlockScales and BlockZeroPoints
use C# 12 collection expressions (= []) which break net471 compatibility;
replace those initializers with Array.Empty<double>() for BlockScales and
Array.Empty<int>() for BlockZeroPoints so the properties become
backward-compatible while preserving empty-array semantics (update the
initializers on the BlockScales and BlockZeroPoints properties).
🧹 Nitpick comments (5)
src/Deployment/Optimization/Quantization/Calibration/CalibrationHelper.cs (2)

439-451: CanRunPredictions always returns true for non-null models.

The try-catch block is ineffective because model != null never throws InvalidOperationException. The method always returns true for any non-null model, making the exception handling dead code.

If the intent is to verify the model is ready for predictions, consider checking a specific property or method that indicates training completion.

♻️ Simplified implementation
 private static bool CanRunPredictions(IFullModel<T, TInput, TOutput> model)
 {
-    // Check if model has been trained and can make predictions
-    try
-    {
-        return model != null;
-    }
-    catch (InvalidOperationException)
-    {
-        // Model not trained or not ready for predictions
-        return false;
-    }
+    // Model is non-null at this point due to caller validation
+    return true;
 }

551-554: Pattern output is T scalar may incorrectly match non-scalar outputs.

When T is a value type (e.g., double), output is T will match any boxed T value. However, since TOutput is a generic type parameter, if TOutput happens to be T itself, this branch catches it correctly. But if TOutput is a collection type that also happens to be T, the pattern could match unexpectedly. Consider moving this branch after the T[] check or adding explicit type exclusions.

src/Deployment/Optimization/Quantization/Formats/FP8Quantizer.cs (1)

61-65: Mode property returns Float16 but this is an FP8 quantizer.

The Mode property returns QuantizationMode.Float16 with a comment "Closest mode", but this could be misleading to callers expecting accurate mode reporting. Consider adding a dedicated FP8 mode to the enum or documenting this limitation more prominently.

src/Deployment/Optimization/Quantization/Strategies/SmoothQuantQuantizer.cs (1)

89-97: Mutating shared configuration object may cause unexpected side effects.

The constructor mutates _config.Strategy if it doesn't match SmoothQuant. If the caller passes a shared configuration instance, this mutation affects all users of that config. Consider creating a defensive copy or throwing an exception for mismatched strategies.

♻️ Defensive copy approach
 public SmoothQuantQuantizer(QuantizationConfiguration? config = null)
 {
-    _config = config ?? QuantizationConfiguration.ForSmoothQuant();
+    var effectiveConfig = config ?? QuantizationConfiguration.ForSmoothQuant();
 
-    if (_config.Strategy != QuantizationStrategy.SmoothQuant)
+    if (effectiveConfig.Strategy != QuantizationStrategy.SmoothQuant)
     {
-        _config.Strategy = QuantizationStrategy.SmoothQuant;
+        // Create a copy to avoid mutating the caller's config
+        effectiveConfig = effectiveConfig.Clone(); // or create new instance with modified strategy
+        effectiveConfig.Strategy = QuantizationStrategy.SmoothQuant;
     }
+    _config = effectiveConfig;
 }
src/Deployment/Optimization/Quantization/Strategies/GPTQQuantizer.cs (1)

70-78: Mutating shared configuration object may cause unexpected side effects.

Same concern as SmoothQuantQuantizer: the constructor mutates _config.Strategy if it doesn't match GPTQ. If the caller passes a shared configuration instance, this mutation affects all users. Consider creating a defensive copy.

Comment thread src/Deployment/Optimization/Quantization/Training/EfficientQATOptimizer.cs Outdated
- CalibrationHelper.cs: fix inverted type assignability check
- EfficientQATOptimizer.cs: use Array.Empty<T>() for net471 compatibility
- SpinQuantQuantizer.cs: refactor to use generic Matrix<T>/Vector<T> types
  instead of hardcoded double and double[,]

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs`:
- Around line 92-95: The code in SpinQuantQuantizer currently mutates the
incoming _config by setting _config.Strategy = QuantizationStrategy.SpinQuant;
instead validate that _config.Strategy equals QuantizationStrategy.SpinQuant and
if it does not, throw a clear ArgumentException/InvalidOperationException
instead of modifying the passed object; update the check in the
SpinQuantQuantizer constructor or initialization method (the block referencing
_config.Strategy and QuantizationStrategy.SpinQuant) to perform validation and
throw to avoid surprising side effects on the caller's config.
🧹 Nitpick comments (1)
src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs (1)

45-45: Unused _zeroPoints dictionary wastes memory.

The _zeroPoints dictionary is allocated but never populated. GetZeroPoint always returns 0 for symmetric quantization. Consider removing this field.

♻️ Proposed fix
     private readonly Dictionary<string, T> _scaleFactors = new();
-    private readonly Dictionary<string, int> _zeroPoints = new();
     private readonly Dictionary<string, Matrix<T>> _rotationMatrices = new();

Comment on lines +437 to +481
{
// Find pivot
int maxRow = col;
for (int row = col + 1; row < n; row++)
{
if (NumOps.Compare(NumOps.Abs(augmented[row, col]), NumOps.Abs(augmented[maxRow, col])) > 0)
{
maxRow = row;
}
}

// Swap rows
for (int j = 0; j < 2 * n; j++)
{
(augmented[col, j], augmented[maxRow, j]) = (augmented[maxRow, j], augmented[col, j]);
}

// Scale pivot row
T pivot = augmented[col, col];
T pivotThreshold = NumOps.FromDouble(1e-10);
if (NumOps.Compare(NumOps.Abs(pivot), pivotThreshold) < 0)
{
// Matrix is singular - add warning and return identity as fallback
_calibrationWarnings.Add($"Warning: Singular matrix detected at column {col} during Cayley transform, falling back to identity rotation.");
return Matrix<T>.CreateIdentity(n);
}

for (int j = 0; j < 2 * n; j++)
{
augmented[col, j] = NumOps.Divide(augmented[col, j], pivot);
}

// Eliminate column
for (int row = 0; row < n; row++)
{
if (row != col)
{
T factor = augmented[row, col];
for (int j = 0; j < 2 * n; j++)
{
augmented[row, j] = NumOps.Subtract(augmented[row, j], NumOps.Multiply(factor, augmented[col, j]));
}
}
}
}

Check notice

Code scanning / CodeQL

Block with too many statements Note

Block with too many statements (4 complex statements in the block).

Copilot Autofix

AI 8 months ago

In general, this kind of issue is best fixed by decomposing the complex loop body into smaller, focused methods. Each helper method should encapsulate a logical sub-step of the algorithm (e.g., finding the pivot row, swapping rows, normalizing the pivot row, and eliminating the current column), so that the main loop becomes a high-level description of the algorithm with fewer complex statements.

For this specific InvertMatrix method in SpinQuantQuantizer.cs, the best approach is to refactor the body of the for (int col = 0; col < n; col++) loop into separate private methods:

  1. FindPivotRow(Matrix<T> augmented, int n, int col) – encapsulates the inner for loop that finds maxRow.
  2. SwapRows(Matrix<T> augmented, int n, int rowA, int rowB) – encapsulates the row-swapping for loop.
  3. bool NormalizePivotRow(Matrix<T> augmented, int n, int col) – encapsulates pivot extraction, singularity check, logging the warning, and scaling the pivot row. It returns false if the matrix is singular so the caller can return the identity fallback.
  4. EliminateColumn(Matrix<T> augmented, int n, int col) – encapsulates the elimination double loop (over row and j).

Then, the for (int col = 0; col < n; col++) loop can call these helpers in sequence, leaving only a small number of complex constructs in the loop body (one if and several method calls instead of nested loops and conditionals). The behavior stays the same: we still use partial pivoting, log the same warning message, and fall back to the identity matrix on singularity.

Concretely:

  • Above InvertMatrix, add the four new private helper methods using the existing fields (NumOps, _calibrationWarnings).
  • Inside the for (int col = 0; col < n; col++) loop, replace the current inlined pivot-finding, swapping, singularity check, scaling, and elimination logic with calls to these helpers.
  • Keep the rest of InvertMatrix (matrix augmentation and inverse extraction) unchanged.

No new imports or external libraries are necessary; everything can be expressed in terms of existing types (Matrix<T>, INumericOperations<T>, _calibrationWarnings).

Suggested changeset 1
src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs b/src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs
--- a/src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs
+++ b/src/Deployment/Optimization/Quantization/Strategies/SpinQuantQuantizer.cs
@@ -419,6 +419,73 @@
     /// <summary>
     /// Inverts a matrix using Gaussian elimination with partial pivoting.
     /// </summary>
+    private int FindPivotRow(Matrix<T> augmented, int n, int col)
+    {
+        int maxRow = col;
+        for (int row = col + 1; row < n; row++)
+        {
+            if (NumOps.Compare(NumOps.Abs(augmented[row, col]), NumOps.Abs(augmented[maxRow, col])) > 0)
+            {
+                maxRow = row;
+            }
+        }
+
+        return maxRow;
+    }
+
+    private void SwapRows(Matrix<T> augmented, int n, int rowA, int rowB)
+    {
+        if (rowA == rowB)
+        {
+            return;
+        }
+
+        for (int j = 0; j < 2 * n; j++)
+        {
+            (augmented[rowA, j], augmented[rowB, j]) = (augmented[rowB, j], augmented[rowA, j]);
+        }
+    }
+
+    private bool NormalizePivotRow(Matrix<T> augmented, int n, int col)
+    {
+        T pivot = augmented[col, col];
+        T pivotThreshold = NumOps.FromDouble(1e-10);
+        if (NumOps.Compare(NumOps.Abs(pivot), pivotThreshold) < 0)
+        {
+            _calibrationWarnings.Add($"Warning: Singular matrix detected at column {col} during Cayley transform, falling back to identity rotation.");
+            return false;
+        }
+
+        for (int j = 0; j < 2 * n; j++)
+        {
+            augmented[col, j] = NumOps.Divide(augmented[col, j], pivot);
+        }
+
+        return true;
+    }
+
+    private void EliminateColumn(Matrix<T> augmented, int n, int col)
+    {
+        for (int row = 0; row < n; row++)
+        {
+            if (row == col)
+            {
+                continue;
+            }
+
+            T factor = augmented[row, col];
+            if (NumOps.Compare(NumOps.Abs(factor), NumOps.Zero) == 0)
+            {
+                continue;
+            }
+
+            for (int j = 0; j < 2 * n; j++)
+            {
+                augmented[row, j] = NumOps.Subtract(augmented[row, j], NumOps.Multiply(factor, augmented[col, j]));
+            }
+        }
+    }
+
     private Matrix<T> InvertMatrix(Matrix<T> matrix)
     {
         int n = matrix.Rows;
@@ -437,49 +504,15 @@
         // Gaussian elimination
         for (int col = 0; col < n; col++)
         {
-            // Find pivot
-            int maxRow = col;
-            for (int row = col + 1; row < n; row++)
-            {
-                if (NumOps.Compare(NumOps.Abs(augmented[row, col]), NumOps.Abs(augmented[maxRow, col])) > 0)
-                {
-                    maxRow = row;
-                }
-            }
+            int maxRow = FindPivotRow(augmented, n, col);
+            SwapRows(augmented, n, col, maxRow);
 
-            // Swap rows
-            for (int j = 0; j < 2 * n; j++)
+            if (!NormalizePivotRow(augmented, n, col))
             {
-                (augmented[col, j], augmented[maxRow, j]) = (augmented[maxRow, j], augmented[col, j]);
-            }
-
-            // Scale pivot row
-            T pivot = augmented[col, col];
-            T pivotThreshold = NumOps.FromDouble(1e-10);
-            if (NumOps.Compare(NumOps.Abs(pivot), pivotThreshold) < 0)
-            {
-                // Matrix is singular - add warning and return identity as fallback
-                _calibrationWarnings.Add($"Warning: Singular matrix detected at column {col} during Cayley transform, falling back to identity rotation.");
                 return Matrix<T>.CreateIdentity(n);
             }
 
-            for (int j = 0; j < 2 * n; j++)
-            {
-                augmented[col, j] = NumOps.Divide(augmented[col, j], pivot);
-            }
-
-            // Eliminate column
-            for (int row = 0; row < n; row++)
-            {
-                if (row != col)
-                {
-                    T factor = augmented[row, col];
-                    for (int j = 0; j < 2 * n; j++)
-                    {
-                        augmented[row, j] = NumOps.Subtract(augmented[row, j], NumOps.Multiply(factor, augmented[col, j]));
-                    }
-                }
-            }
+            EliminateColumn(augmented, n, col);
         }
 
         // Extract inverse
EOF
Copilot is powered by AI and may make mistakes. Always verify output.

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

…ating config

SpinQuantQuantizer now throws ArgumentException if Strategy is not SpinQuant,
instead of silently modifying the caller's config object.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Models/Results/AiModelResult.cs (1)

1162-1167: ⚠️ Potential issue | 🟠 Major

QuantizationInfo is dropped on deserialize/clone paths.
Line 1166 assigns from options, but Deserialize, WithParameters, and DeepCopy don’t propagate it, so loaded/cloned results lose quantization metadata. Please carry it through those paths.

🛠️ Proposed fix
@@ public void Deserialize(byte[] data)
-                InferenceOptimizationConfig = deserializedObject.InferenceOptimizationConfig;
+                InferenceOptimizationConfig = deserializedObject.InferenceOptimizationConfig;
+                QuantizationInfo = deserializedObject.QuantizationInfo;
                 SerializedModelData = deserializedObject.SerializedModelData;
@@ public IFullModel<T, TInput, TOutput> WithParameters(Vector<T> parameters)
             // JIT compilation is parameter-specific, don't copy
             InferenceOptimizationConfig = InferenceOptimizationConfig,
+            QuantizationInfo = QuantizationInfo,
             ReasoningConfig = ReasoningConfig,
@@ public IFullModel<T, TInput, TOutput> DeepCopy()
             // JIT compilation is model-specific, don't copy
             InferenceOptimizationConfig = InferenceOptimizationConfig,
+            QuantizationInfo = QuantizationInfo,
             ReasoningConfig = ReasoningConfig,

Copilot AI 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.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 11 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +82
private static (double scale, int zeroPoint) ComputeQuantizationParameters(
double maxAbs, double minVal, double maxVal, int bitWidth, double minScaleFactor, bool symmetric)
{
double qMax = symmetric ? (1 << (bitWidth - 1)) - 1 : (1 << bitWidth) - 1;
double scale;
int zeroPoint;

if (symmetric)
{
scale = maxAbs / qMax;
zeroPoint = 0;
}
else
{
scale = (maxVal - minVal) / ((1 << bitWidth) - 1);
scale = Math.Max(scale, minScaleFactor);
zeroPoint = (int)MathHelper.Clamp(Math.Round(-minVal / scale), 0, qMax);
}

// Ensure minimum scale factor
scale = Math.Max(scale, minScaleFactor);
return (scale, zeroPoint);
}

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

In the ComputeQuantizationParameters method, the symmetric parameter is declared but when calculating the zero point for asymmetric quantization, the code uses MathHelper.Clamp(Math.Round(-minVal / scale), 0, qMax). However, in asymmetric mode, qMax is calculated as (1 << bitWidth) - 1, which means the valid range should be [0, qMax]. The clamping is correct, but the calculation of qMax in the symmetric branch uses (1 << (bitWidth - 1)) - 1 which is different. This inconsistency could lead to incorrect quantization ranges. The symmetric qMax calculation should match the pattern used in other quantizers for consistency.

Copilot uses AI. Check for mistakes.
Comment on lines +312 to +344
public static double ByteToE4M3(byte b)
{
if (b == 0) return 0;

bool sign = (b & 0x80) != 0;
int exp = (b >> 3) & 0x0F;
int mantissa = b & 0x07;

double value;
if (exp == 0)
{
// Subnormal
value = mantissa / 8.0 * Math.Pow(2, -6);
}
else if (exp == 15)
{
// E4M3 supports NaN but not Infinity per NVIDIA spec
// NaN is encoded when exp=15 and mantissa=7 (0x7F or 0xFF)
if (mantissa == 7)
{
return double.NaN;
}
// Other exp=15 patterns represent max value
value = E4M3_MAX_VALUE;
}
else
{
// Normal
value = (1.0 + mantissa / 8.0) * Math.Pow(2, exp - E4M3_EXPONENT_BIAS);
}

return sign ? -value : value;
}

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

The ByteToE4M3 method handles NaN by checking if mantissa == 7 when exp == 15. However, according to the NVIDIA E4M3 spec, there are multiple NaN encodings (all patterns with exp=15 and mantissa != 0 should be NaN). The current implementation only recognizes 0x7F and 0xFF as NaN, but patterns like 0x79-0x7E and 0xF9-0xFE should also be treated as NaN. This could lead to incorrect interpretation of E4M3 values.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +50
// NF4 codebook: 16 values used in QLoRA for N(0,1)-distributed weights.
// These are empirically optimized representative values for equal-probability bins of N(0,1),
// not exact analytical quantiles. The interval comments below show approximate quantile ranges
// for each bin. See Dettmers et al., "QLoRA: Efficient Finetuning of Quantized LLMs" (2023),
// Appendix / NF4 codebook definition, for the original table of values.

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

The comment on lines 47-50 describes the NF4 codebook as "empirically optimized representative values" but then says the interval comments show "approximate quantile ranges". The actual NF4 codebook values from the QLoRA paper are quantile centers for a standard normal distribution N(0,1), not empirically optimized values. The comment should accurately reflect that these are analytically derived quantile centers, not empirical optimizations. This is an important distinction for users who want to understand the theoretical foundation of NF4.

Suggested change
// NF4 codebook: 16 values used in QLoRA for N(0,1)-distributed weights.
// These are empirically optimized representative values for equal-probability bins of N(0,1),
// not exact analytical quantiles. The interval comments below show approximate quantile ranges
// for each bin. See Dettmers et al., "QLoRA: Efficient Finetuning of Quantized LLMs" (2023),
// Appendix / NF4 codebook definition, for the original table of values.
// NF4 codebook: 16 quantile-center values for N(0,1) used in QLoRA for normally distributed weights.
// These are analytically derived centers of equal-probability quantization bins of a standard normal
// distribution, not empirically optimized values. The interval comments below show approximate bin
// boundaries (quantile ranges) for each level. See Dettmers et al., "QLoRA: Efficient Finetuning of
// Quantized LLMs" (2023), Appendix / NF4 codebook definition, for the original table of values.

Copilot uses AI. Check for mistakes.
Comment on lines +268 to +305
/// <summary>
/// Gets the processing order for columns based on activation importance (ActOrder optimization).
/// </summary>
private int[] GetProcessingOrder(double[] weights, int n, bool useActOrder)
{
int[] order = new int[n];

if (useActOrder && _activationStats?.GlobalActivationMagnitudes != null &&
_activationStats.GlobalActivationMagnitudes.Length > 0)
{
// Compute activation importance scores from calibration statistics
var importance = new (int index, double score)[n];
var magnitudes = _activationStats.GlobalActivationMagnitudes;

for (int i = 0; i < n; i++)
{
double score = i < magnitudes.Length ? magnitudes[i] : 0;
importance[i] = (i, score);
}

// Sort by importance (descending) - process most important first
var sorted = importance.OrderByDescending(x => x.score).ToArray();
for (int i = 0; i < n; i++)
{
order[i] = sorted[i].index;
}
}
else
{
// Default order
for (int i = 0; i < n; i++)
{
order[i] = i;
}
}

return order;
}

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

In the GetProcessingOrder method, when useActOrder is true and activation statistics are available, the code sorts by importance descending and then stores the sorted indices in the order array. However, the comment says "process most important first", but the actual logic in QuantizeWithGPTQ processes columns in the order specified by the order array from i = groupStart to i < groupEnd, using idx = order[i]. This means if order[0] contains the most important index, it will be processed first, which is correct. However, the variable naming could be clearer - calling it order suggests it's a permutation, but it's actually an array where order[i] gives the parameter index to process at step i.

Copilot uses AI. Check for mistakes.
Comment on lines +398 to +409
// s_j = max(|X_j|)^α / max(|W_j|)^(1-α)
// This balances the quantization difficulty between X and W

double actMax = Math.Max(activationMax[i], 1e-6);
double wMax = Math.Max(weightMax[i], 1e-6);

// Use log-space computation for numerical stability when values are very small
// s = exp(alpha * log(actMax) - (1-alpha) * log(wMax))
double logAct = Math.Log(actMax);
double logW = Math.Log(wMax);
double logS = alpha * logAct - (1.0 - alpha) * logW;
double s = Math.Exp(logS);

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

In the ComputeSmoothingScales method, the log-space computation uses logS = alpha * logAct - (1.0 - alpha) * logW to compute the smoothing scale. However, the formula comment states s_j = max(|X_j|)^α / max(|W_j|)^(1-α). Taking logs: log(s) = α*log(act) - (1-α)*log(w). The current implementation matches this, but it's actually computing s = act^α / w^(1-α), which should be log(s) = α*log(act) - (1-α)*log(w). However, the exponent on w should be (1-α), so the log should be - (1-α)*log(w) = -(log(w) - α*log(w)). The current formula is correct, but the comment could be clearer about the log-space transformation.

Copilot uses AI. Check for mistakes.
/// <item><description>Any type with ToArray() method returning T[] (via reflection)</description></item>
/// </list>
/// <para>The reflection fallback is expensive and should be avoided for performance-critical code.
/// If the ToArray() method exists but returns a different type (not T[]), conversion will fail silently.</para>

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

The ConvertToTensor method uses reflection as a fallback to call ToArray() on unknown types. The comment on lines 467-468 states "This fallback is expensive and should be avoided for performance-critical code" and "If the ToArray() method exists but returns a different type (not T[]), conversion will fail silently." However, the actual code on lines 500-509 does check the return type before invoking: if (returnType == typeof(T[]) || typeof(T[]).IsAssignableFrom(returnType)). This check prevents the silent failure mentioned in the comment. The comment should be updated to reflect that the method validates the return type before invoking, and only fails silently if the method throws or returns null.

Suggested change
/// If the ToArray() method exists but returns a different type (not T[]), conversion will fail silently.</para>
/// The method validates that ToArray() returns a compatible T[] before invoking; conversion will only
/// fail silently if ToArray() throws or returns null, in which case this method simply returns null.</para>

Copilot uses AI. Check for mistakes.
Comment on lines +415 to +428
// Map global indices to local indices within the hessianDiag array
if (hessianDiag.Length == 0) return _config.GPTQDampingFactor;

int iLocal = i % hessianDiag.Length;
int jLocal = j % hessianDiag.Length;

// Both indices are guaranteed to be in range after modulo
if (iLocal >= 0 && jLocal >= 0)
{
return Math.Sqrt(hessianDiag[iLocal] * hessianDiag[jLocal]) * 0.1;
}

// Fallback for out-of-range indices: use damping factor
return _config.GPTQDampingFactor;

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

The GetHessianCrossElement method uses modulo operation to map global indices to local indices: int iLocal = i % hessianDiag.Length and int jLocal = j % hessianDiag.Length. This assumes that indices wrap around, which may not accurately represent the cross-element structure of the Hessian matrix. For a proper GPTQ implementation, cross-elements should come from the inverse Hessian matrix computed via Cholesky decomposition, but this simplified version uses a geometric mean approximation. The modulo mapping could cause incorrect cross-element values when processing large groups with ActOrder, potentially degrading quantization quality.

Suggested change
// Map global indices to local indices within the hessianDiag array
if (hessianDiag.Length == 0) return _config.GPTQDampingFactor;
int iLocal = i % hessianDiag.Length;
int jLocal = j % hessianDiag.Length;
// Both indices are guaranteed to be in range after modulo
if (iLocal >= 0 && jLocal >= 0)
{
return Math.Sqrt(hessianDiag[iLocal] * hessianDiag[jLocal]) * 0.1;
}
// Fallback for out-of-range indices: use damping factor
return _config.GPTQDampingFactor;
// Validate indices against the local hessianDiag array. If indices fall outside
// the local group, treat them as weakly correlated and use the damping factor,
// rather than wrapping via modulo which can mix unrelated parameters.
if (hessianDiag.Length == 0)
return _config.GPTQDampingFactor;
if (i < 0 || i >= hessianDiag.Length || j < 0 || j >= hessianDiag.Length)
return _config.GPTQDampingFactor;
return Math.Sqrt(hessianDiag[i] * hessianDiag[j]) * 0.1;

Copilot uses AI. Check for mistakes.
// Grid search over scaling factors (configurable via AWQScaleSearchOptions)
double[] scaleOptions = config.AWQScaleSearchOptions.Length > 0
? config.AWQScaleSearchOptions
: [0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0];

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

In the FindOptimalScale method, the grid search uses the configured AWQScaleSearchOptions with a default of [0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0]. However, scale values above 1.0 would actually amplify weights rather than protect them. The AWQ paper typically uses scale values in the range [0.0, 1.0] for the protection factor. Values like 1.5 and 2.0 seem incorrect - they would make protected weights MORE susceptible to quantization error, not less. This could lead to suboptimal quantization results.

Suggested change
: [0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0, 1.5, 2.0];
: [0.0, 0.1, 0.2, 0.3, 0.5, 0.7, 1.0];

Copilot uses AI. Check for mistakes.
Comment on lines +74 to +83
/// Computes the zero-point for asymmetric quantization.
/// </summary>
/// <param name="min">The minimum value in the range.</param>
/// <param name="scale">The quantization scale factor.</param>
/// <param name="qMax">The maximum quantized value.</param>
/// <returns>The clamped zero-point value.</returns>
private static int ComputeAsymmetricZeroPoint(double min, double scale, double qMax)
{
return (int)MathHelper.Clamp(Math.Round(-min / scale), 0, qMax);
}

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

The ComputeAsymmetricZeroPoint method is defined as a static local function, but it's only called twice within the same file. While this is not incorrect, the method could be made an instance method or a private static method of the class for better discoverability and reusability. The current placement as a static local function at the top of the class (line 75-83) is unusual - static local functions are typically defined closer to their usage point. Consider moving this to be a private static method of the class.

Copilot uses AI. Check for mistakes.
Comment on lines +188 to +233
model.SetTrainingMode(false);
try
{
foreach (var inputTensor in samples)
{
totalSamples++;
try
{
// Run forward pass with memory to capture intermediate activations
var output = model.ForwardWithMemory(inputTensor);

// Store output activation stats
if (!stats.LayerStats.TryGetValue("output", out var outputStats))
{
outputStats = new LayerActivationStats<T> { LayerName = "output" };
stats.LayerStats["output"] = outputStats;
}
outputStats.Update(output);

// Store input activation stats
if (!stats.LayerStats.TryGetValue("input", out var inputStats))
{
inputStats = new LayerActivationStats<T> { LayerName = "input" };
stats.LayerStats["input"] = inputStats;
}
inputStats.Update(inputTensor);

stats.SampleCount++;
}
catch (Exception ex)
{
failedSamples++;
// Log first few failure reasons for debugging
if (failedSamples <= 3)
{
stats.CalibrationWarnings.Add($"Forward pass sample {totalSamples} failed: {ex.Message}");
}
continue;
}
}
}
finally
{
// Restore training mode
model.SetTrainingMode(true);
}

Copilot AI Feb 4, 2026

Copy link

Choose a reason for hiding this comment

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

The CollectTensorBasedActivations method calls model.SetTrainingMode(false) to set inference mode for calibration, then uses a try-finally block to restore training mode with model.SetTrainingMode(true) in the finally block (lines 188-233). However, this assumes the model was in training mode before calibration started. If the model was already in inference mode, this would incorrectly switch it to training mode after calibration. The code should save the current training mode state before changing it and restore the original state in the finally block.

Copilot uses AI. Check for mistakes.
Comment thread src/AiModelBuilder.cs
{
calibrationData = xTrainArray.Take(Math.Min(calibrationSampleCount, xTrainArray.Length));
}
else if (XTrain is IEnumerable<TInput> xTrainEnumerable)

Check warning

Code scanning / CodeQL

Constant condition Warning

Condition is always not null because of
... != ...
.

Copilot Autofix

AI 8 months ago

In general, to fix this type of issue you should avoid using null-conditional operators or other null checks inside a scope that is already guarded by a non-null check on the same variable. Doing so keeps the control flow clear and prevents static analyzers from reporting constant-condition issues.

Concretely here, the if statement on line 2323:

if (_quantizationConfig != null && _quantizationConfig.Mode != QuantizationMode.None && optimizationResult.BestSolution != null)

guarantees _quantizationConfig is non-null in the body. Therefore, on line 2329 we can safely replace:

int calibrationSampleCount = _quantizationConfig?.CalibrationSamples ?? 100;

with a direct access:

int calibrationSampleCount = _quantizationConfig.CalibrationSamples ?? 100;

This preserves existing functionality: if CalibrationSamples is a nullable int?, the null-coalescing ?? 100 still applies; if it is a non-nullable int, the ?? 100 is redundant but harmless (and you may optionally keep it to avoid changing behavior). No new methods or imports are required; the change is entirely local to the quantization block in src/AiModelBuilder.cs.

Suggested changeset 1
src/AiModelBuilder.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/AiModelBuilder.cs b/src/AiModelBuilder.cs
--- a/src/AiModelBuilder.cs
+++ b/src/AiModelBuilder.cs
@@ -2326,7 +2326,7 @@
             {
                 // Use preprocessed training data as calibration data for consistent quantization
                 // This ensures calibration sees the same data distribution as during training
-                int calibrationSampleCount = _quantizationConfig?.CalibrationSamples ?? 100;
+                int calibrationSampleCount = _quantizationConfig.CalibrationSamples ?? 100;
                 IEnumerable<TInput>? calibrationData = null;
                 if (XTrain is TInput[] xTrainArray)
                 {
EOF
@@ -2326,7 +2326,7 @@
{
// Use preprocessed training data as calibration data for consistent quantization
// This ensures calibration sees the same data distribution as during training
int calibrationSampleCount = _quantizationConfig?.CalibrationSamples ?? 100;
int calibrationSampleCount = _quantizationConfig.CalibrationSamples ?? 100;
IEnumerable<TInput>? calibrationData = null;
if (XTrain is TInput[] xTrainArray)
{
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread src/AiModelBuilder.cs
{
calibrationData = xTrainArray.Take(Math.Min(calibrationSampleCount, xTrainArray.Length));
}
else if (XTrain is IEnumerable<TInput> xTrainEnumerable)

Check warning

Code scanning / CodeQL

Constant condition Warning

Condition is always not null because of
... != ...
.

Copilot Autofix

AI 8 months ago

In general, to fix a constant condition arising from a redundant null‑conditional access after a prior null check, you remove the unnecessary null‑conditional operator and treat the value as non‑null within the guarded block. This both simplifies the logic and aligns with what the earlier null check already guarantees.

Concretely, in src/AiModelBuilder.cs, at line 2323, there is an if block guarded by _quantizationConfig != null. Inside that block, line 2329 uses _quantizationConfig?.CalibrationSamples ?? 100;. Because _quantizationConfig cannot be null within this block, the ?. is unnecessary and leads to a constant condition warning. Replace _quantizationConfig?.CalibrationSamples with _quantizationConfig.CalibrationSamples. No new methods or imports are needed; this is a local expression change that does not alter functionality, because the ?? 100 already handles the case where CalibrationSamples itself may be nullable or has a default value.

Suggested changeset 1
src/AiModelBuilder.cs

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/AiModelBuilder.cs b/src/AiModelBuilder.cs
--- a/src/AiModelBuilder.cs
+++ b/src/AiModelBuilder.cs
@@ -2326,7 +2326,7 @@
             {
                 // Use preprocessed training data as calibration data for consistent quantization
                 // This ensures calibration sees the same data distribution as during training
-                int calibrationSampleCount = _quantizationConfig?.CalibrationSamples ?? 100;
+                int calibrationSampleCount = _quantizationConfig.CalibrationSamples ?? 100;
                 IEnumerable<TInput>? calibrationData = null;
                 if (XTrain is TInput[] xTrainArray)
                 {
EOF
@@ -2326,7 +2326,7 @@
{
// Use preprocessed training data as calibration data for consistent quantization
// This ensures calibration sees the same data distribution as during training
int calibrationSampleCount = _quantizationConfig?.CalibrationSamples ?? 100;
int calibrationSampleCount = _quantizationConfig.CalibrationSamples ?? 100;
IEnumerable<TInput>? calibrationData = null;
if (XTrain is TInput[] xTrainArray)
{
Copilot is powered by AI and may make mistakes. Always verify output.
@ooples
ooples merged commit c0c6a57 into master Feb 4, 2026
45 of 47 checks passed
@ooples
ooples deleted the feature/quantization-ptq-qat-278 branch February 4, 2026 21:32
@sonarqubecloud

sonarqubecloud Bot commented Feb 4, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)
5.0% Duplication on New Code (required ≤ 3%)
B Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

ooples added a commit that referenced this pull request Jul 19, 2026
…0.117.0)

Bumps the AiDotNet.Tensors pin 0.116.0 -> 0.117.0, which ships the optional attention-logit
soft-cap on ScaledDotProductAttention (CPU + all six GPU backends, Tensors #816), and wires the
Gemma-2 attn_logit_softcapping path through the facade import:

  - GroupedQueryAttentionLayer takes an optional attnLogitSoftcap and forwards it into the fused
    Engine.ScaledDotProductAttention(..., softcap); exposed via the AttnLogitSoftcap property. The
    (never-faithful) softcap + ALiBi combination throws rather than silently dropping the cap.
  - HuggingFaceConfig parses attn_logit_softcapping (alongside the existing final_logit_softcapping).
  - Gemma2ModelBuilder threads config.AttnLogitSoftcapping into every decoder block's attention.

Also fixes a pre-existing net471 compile break in the pretrained/GGUF test files: TensorShape does
not convert to IEnumerable<int> on net471, so Assert.Equal(int[], x.Shape) failed to compile there
(these files only ever ran on net10.0). Comparing x.Shape.ToArray() builds on both TFMs.

Test: Builder_Gemma2_AttnLogitSoftcap_ParsesAndAltersAttention asserts the config parses, the builder
threads the cap into the layer (structural), and a bite-sized cap actually changes the forward output
(functional). Pretrained + GGUF suites 36/36 green; both TFMs build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ooples added a commit that referenced this pull request Jul 21, 2026
…0.117.0)

Bumps the AiDotNet.Tensors pin 0.116.0 -> 0.117.0, which ships the optional attention-logit
soft-cap on ScaledDotProductAttention (CPU + all six GPU backends, Tensors #816), and wires the
Gemma-2 attn_logit_softcapping path through the facade import:

  - GroupedQueryAttentionLayer takes an optional attnLogitSoftcap and forwards it into the fused
    Engine.ScaledDotProductAttention(..., softcap); exposed via the AttnLogitSoftcap property. The
    (never-faithful) softcap + ALiBi combination throws rather than silently dropping the cap.
  - HuggingFaceConfig parses attn_logit_softcapping (alongside the existing final_logit_softcapping).
  - Gemma2ModelBuilder threads config.AttnLogitSoftcapping into every decoder block's attention.

Also fixes a pre-existing net471 compile break in the pretrained/GGUF test files: TensorShape does
not convert to IEnumerable<int> on net471, so Assert.Equal(int[], x.Shape) failed to compile there
(these files only ever ran on net10.0). Comparing x.Shape.ToArray() builds on both TFMs.

Test: Builder_Gemma2_AttnLogitSoftcap_ParsesAndAltersAttention asserts the config parses, the builder
threads the cap into the layer (structural), and a bite-sized cap actually changes the forward output
(functional). Pretrained + GGUF suites 36/36 green; both TFMs build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 7b9ca140 Deployed Feb 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Feature work item roadmap Roadmap-tracked item

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Quantization – PTQ and QAT (INT8/INT4) with LoRA Compatibility

4 participants