Packages

MLX machine learning framework bindings for Erlang

Current section

Files

Jump to
mlx src mlx_quantization.erl
Raw

src/mlx_quantization.erl

-module(mlx_quantization).
%% Advanced quantization for efficient inference and training
-export([
%% Quantization schemes
quantize_int8/2, quantize_int8/3,
quantize_int4/2, quantize_int4/3,
quantize_dynamic/1, quantize_dynamic/2,
%% Dequantization
dequantize/3, dequantize/4,
%% Mixed precision
enable_mixed_precision/0, disable_mixed_precision/0,
autocast/1, autocast/2,
%% Quantization-aware training
fake_quantize/3, fake_quantize/4,
straight_through_estimator/3,
%% Calibration
calibrate_int8/2, calibrate_int8/3,
collect_stats/2, compute_quantization_params/2,
%% Weight quantization
quantize_weights/2, quantize_weights/3,
quantize_activations/2, quantize_activations/3,
%% Advanced quantization methods
quantize_qdq/3, quantize_qdq/4,
quantize_smooth/3, quantize_smooth/4,
quantize_outlier_aware/3,
%% Bit manipulation
pack_int4/1, unpack_int4/1,
pack_bits/2, unpack_bits/2,
%% Quantized operations
quantized_linear/4, quantized_linear/5,
quantized_conv2d/4, quantized_conv2d/5,
quantized_matmul/4, quantized_matmul/5,
%% Sparsity-aware quantization
sparse_quantize/3, sparse_quantize/4,
structured_prune_quantize/4,
%% Model compression
compress_model/2, decompress_model/1,
estimate_compression_ratio/2
]).
%% Quantization schemes
quantize_int8(Array, Scale) ->
quantize_int8(Array, Scale, 0).
quantize_int8(Array, Scale, ZeroPoint) ->
% Quantize to 8-bit integers
{ok, Scaled} = mlx:divide(Array, Scale),
{ok, Shifted} = mlx:add(Scaled, mlx:array(ZeroPoint)),
{ok, Rounded} = mlx:round(Shifted),
{ok, Clamped} = mlx:clip(Rounded, mlx:array(-128), mlx:array(127)),
mlx:cast(Clamped, int8).
quantize_int4(Array, Scale) ->
quantize_int4(Array, Scale, 0).
quantize_int4(Array, Scale, ZeroPoint) ->
% Quantize to 4-bit integers
{ok, Scaled} = mlx:divide(Array, Scale),
{ok, Shifted} = mlx:add(Scaled, mlx:array(ZeroPoint)),
{ok, Rounded} = mlx:round(Shifted),
{ok, Clamped} = mlx:clip(Rounded, mlx:array(-8), mlx:array(7)),
mlx:cast(Clamped, int8).
quantize_dynamic(Array) ->
quantize_dynamic(Array, int8).
quantize_dynamic(Array, TargetType) ->
% Dynamic quantization with automatic scale computation
{ok, MinVal} = mlx:min(Array),
{ok, MaxVal} = mlx:max(Array),
{QMin, QMax} = case TargetType of
int8 -> {-128, 127};
int4 -> {-8, 7};
uint8 -> {0, 255}
end,
% Compute scale and zero point
{ok, Range} = mlx:subtract(MaxVal, MinVal),
{ok, QRange} = mlx:array(QMax - QMin),
{ok, Scale} = mlx:divide(Range, QRange),
{ok, ZeroPointFloat} = mlx:subtract(mlx:array(QMin),
mlx:divide(MinVal, Scale)),
{ok, ZeroPoint} = mlx:round(ZeroPointFloat),
{ok, ZeroPointClamped} = mlx:clip(ZeroPoint,
mlx:array(QMin), mlx:array(QMax)),
% Quantize
case TargetType of
int8 -> quantize_int8(Array, Scale, ZeroPointClamped);
int4 -> quantize_int4(Array, Scale, ZeroPointClamped);
uint8 -> quantize_uint8(Array, Scale, ZeroPointClamped)
end.
%% Dequantization
dequantize(QuantizedArray, Scale, ZeroPoint) ->
dequantize(QuantizedArray, Scale, ZeroPoint, float32).
dequantize(QuantizedArray, Scale, ZeroPoint, TargetType) ->
% Dequantize back to floating point
{ok, FloatArray} = mlx:cast(QuantizedArray, float32),
{ok, Shifted} = mlx:subtract(FloatArray, ZeroPoint),
{ok, Scaled} = mlx:multiply(Shifted, Scale),
case TargetType of
float32 -> {ok, Scaled};
float16 -> mlx:cast(Scaled, float16);
bfloat16 -> mlx:cast(Scaled, bfloat16)
end.
%% Mixed precision
enable_mixed_precision() ->
put(mixed_precision_enabled, true),
put(loss_scale, 65536.0),
ok.
disable_mixed_precision() ->
put(mixed_precision_enabled, false),
erase(loss_scale),
ok.
autocast(Fun) ->
autocast(Fun, #{}).
autocast(Fun, Options) ->
% Automatic mixed precision casting
case get(mixed_precision_enabled) of
true ->
% Save current dtypes
OldDtype = get(default_dtype),
put(default_dtype, float16),
try
Result = Fun(),
% Scale loss if this is a loss computation
case maps:get(scale_loss, Options, false) of
true ->
LossScale = get(loss_scale),
{ok, ScaledResult} = mlx:multiply(Result, mlx:array(LossScale)),
{ok, ScaledResult};
false ->
{ok, Result}
end
after
put(default_dtype, OldDtype)
end;
_ ->
{ok, Fun()}
end.
%% Quantization-aware training
fake_quantize(Array, Scale, ZeroPoint) ->
fake_quantize(Array, Scale, ZeroPoint, int8).
fake_quantize(Array, Scale, ZeroPoint, QuantType) ->
% Fake quantization for training
{QMin, QMax} = case QuantType of
int8 -> {-128, 127};
int4 -> {-8, 7};
uint8 -> {0, 255}
end,
% Forward: quantize then dequantize
{ok, Scaled} = mlx:divide(Array, Scale),
{ok, Shifted} = mlx:add(Scaled, ZeroPoint),
{ok, Quantized} = mlx:clip(mlx:round(Shifted),
mlx:array(QMin), mlx:array(QMax)),
{ok, Dequantized} = mlx:subtract(Quantized, ZeroPoint),
mlx:multiply(Dequantized, Scale).
straight_through_estimator(Array, Scale, ZeroPoint) ->
% Straight-through estimator for gradient flow
case fake_quantize(Array, Scale, ZeroPoint) of
{ok, FakeQuantized} ->
% In backward pass, gradients flow through unchanged
mlx_nif:straight_through(Array, FakeQuantized);
Error -> Error
end.
%% Calibration
calibrate_int8(Model, CalibrationData) ->
calibrate_int8(Model, CalibrationData, #{}).
calibrate_int8(Model, CalibrationData, Options) ->
% Calibrate quantization parameters using representative data
NumBatches = maps:get(num_batches, Options, 100),
Percentile = maps:get(percentile, Options, 99.99),
% Collect activation statistics
Stats = lists:foldl(fun(Batch, Acc) ->
% Forward pass to collect stats
mlx_autograd:no_grad(fun() ->
_Output = Model(Batch),
% Collect intermediate activations
collect_activation_stats(Acc)
end)
end, #{}, lists:sublist(CalibrationData, NumBatches)),
% Compute quantization parameters from stats
QuantParams = maps:map(fun(_Layer, LayerStats) ->
compute_quantization_params(LayerStats, Percentile)
end, Stats),
{ok, QuantParams}.
collect_stats(Arrays, ExistingStats) ->
% Collect min/max statistics for quantization
lists:foldl(fun(Array, Acc) ->
{ok, Min} = mlx:min(Array),
{ok, Max} = mlx:max(Array),
LayerId = get_layer_id(Array),
CurrentStats = maps:get(LayerId, Acc, #{mins => [], maxs => []}),
NewStats = #{
mins => [Min | maps:get(mins, CurrentStats, [])],
maxs => [Max | maps:get(maxs, CurrentStats, [])]
},
maps:put(LayerId, NewStats, Acc)
end, ExistingStats, Arrays).
compute_quantization_params(Stats, Percentile) ->
% Compute scale and zero point from collected statistics
#{mins := Mins, maxs := Maxs} = Stats,
% Use percentile to handle outliers
{ok, MinTensor} = mlx:stack(Mins, 0),
{ok, MaxTensor} = mlx:stack(Maxs, 0),
{ok, MinPercentile} = mlx_nif:percentile(MinTensor, (100 - Percentile) / 2),
{ok, MaxPercentile} = mlx_nif:percentile(MaxTensor, (100 + Percentile) / 2),
% Compute symmetric scale
{ok, AbsMax} = mlx:maximum(mlx:abs(MinPercentile), mlx:abs(MaxPercentile)),
{ok, Scale} = mlx:divide(AbsMax, mlx:array(127)),
#{scale => Scale, zero_point => 0}.
%% Weight quantization
quantize_weights(Weights, Method) ->
quantize_weights(Weights, Method, #{}).
quantize_weights(Weights, Method, Options) ->
% Quantize model weights
case Method of
int8_symmetric ->
maps:map(fun(_Name, Weight) ->
{ok, Scale} = compute_symmetric_scale(Weight, 8),
quantize_int8(Weight, Scale, 0)
end, Weights);
int4_symmetric ->
maps:map(fun(_Name, Weight) ->
{ok, Scale} = compute_symmetric_scale(Weight, 4),
quantize_int4(Weight, Scale, 0)
end, Weights);
gptq ->
gptq_quantize(Weights, Options);
awq ->
awq_quantize(Weights, Options)
end.
quantize_activations(Activations, Method) ->
quantize_activations(Activations, Method, #{}).
quantize_activations(Activations, Method, Options) ->
% Quantize activations using calibration data
case Method of
int8_dynamic ->
lists:map(fun(Act) ->
quantize_dynamic(Act, int8)
end, Activations);
int8_static ->
CalibParams = maps:get(calib_params, Options, #{}),
lists:map(fun(Act) ->
LayerId = get_layer_id(Act),
#{scale := Scale, zero_point := ZeroPoint} = maps:get(LayerId, CalibParams),
quantize_int8(Act, Scale, ZeroPoint)
end, Activations)
end.
%% Advanced quantization methods
quantize_qdq(Array, Scale, ZeroPoint) ->
quantize_qdq(Array, Scale, ZeroPoint, int8).
quantize_qdq(Array, Scale, ZeroPoint, QuantType) ->
% Quantize-Dequantize (QDQ) operation
case fake_quantize(Array, Scale, ZeroPoint, QuantType) of
{ok, Result} -> {ok, Result};
Error -> Error
end.
quantize_smooth(Array, Alpha, Scale) ->
quantize_smooth(Array, Alpha, Scale, int8).
quantize_smooth(Array, Alpha, Scale, QuantType) ->
% SmoothQuant: smooth activation outliers to weights
{ok, SmoothedArray} = mlx:divide(Array, mlx:power(Alpha, mlx:array(0.5))),
{ok, AdjustedScale} = mlx:multiply(Scale, mlx:power(Alpha, mlx:array(0.5))),
quantize_int8(SmoothedArray, AdjustedScale, 0).
quantize_outlier_aware(Array, OutlierThreshold, Scale) ->
% Outlier-aware quantization
{ok, AbsArray} = mlx:abs(Array),
{ok, OutlierMask} = mlx:greater(AbsArray, OutlierThreshold),
% Separate outliers and normal values
{ok, NormalValues} = mlx:where(OutlierMask, mlx:array(0.0), Array),
{ok, OutlierValues} = mlx:where(OutlierMask, Array, mlx:array(0.0)),
% Quantize normal values
{ok, QuantizedNormal} = quantize_int8(NormalValues, Scale, 0),
% Keep outliers in FP16
{ok, OutliersFP16} = mlx:cast(OutlierValues, float16),
{ok, {QuantizedNormal, OutliersFP16, OutlierMask}}.
%% Bit manipulation
pack_int4(Array) ->
% Pack two 4-bit values into one 8-bit value
mlx_nif:pack_int4(Array).
unpack_int4(PackedArray) ->
% Unpack 8-bit values into two 4-bit values
mlx_nif:unpack_int4(PackedArray).
pack_bits(Array, NumBits) ->
% Pack values into specified bit width
mlx_nif:pack_bits(Array, NumBits).
unpack_bits(PackedArray, NumBits) ->
% Unpack values from specified bit width
mlx_nif:unpack_bits(PackedArray, NumBits).
%% Quantized operations
quantized_linear(Input, QuantWeight, Scale, ZeroPoint) ->
quantized_linear(Input, QuantWeight, Scale, ZeroPoint, undefined).
quantized_linear(Input, QuantWeight, Scale, ZeroPoint, Bias) ->
% Quantized linear layer
{ok, DequantWeight} = dequantize(QuantWeight, Scale, ZeroPoint),
mlx_nn:linear(Input, DequantWeight, Bias).
quantized_conv2d(Input, QuantWeight, Scale, ZeroPoint) ->
quantized_conv2d(Input, QuantWeight, Scale, ZeroPoint, #{}).
quantized_conv2d(Input, QuantWeight, Scale, ZeroPoint, Options) ->
% Quantized 2D convolution
{ok, DequantWeight} = dequantize(QuantWeight, Scale, ZeroPoint),
Stride = maps:get(stride, Options, 1),
Padding = maps:get(padding, Options, 0),
Bias = maps:get(bias, Options, undefined),
mlx_nn:conv2d(Input, DequantWeight, Bias, Stride, Padding).
quantized_matmul(A, QuantB, Scale, ZeroPoint) ->
quantized_matmul(A, QuantB, Scale, ZeroPoint, float32).
quantized_matmul(A, QuantB, Scale, ZeroPoint, OutputType) ->
% Quantized matrix multiplication
{ok, DequantB} = dequantize(QuantB, Scale, ZeroPoint),
{ok, Result} = mlx:matmul(A, DequantB),
case OutputType of
float32 -> {ok, Result};
_ -> mlx:cast(Result, OutputType)
end.
%% Sparsity-aware quantization
sparse_quantize(Array, Mask, Scale) ->
sparse_quantize(Array, Mask, Scale, int8).
sparse_quantize(Array, Mask, Scale, QuantType) ->
% Quantize only non-sparse elements
{ok, SparseArray} = mlx:multiply(Array, Mask),
{ok, QuantizedSparse} = case QuantType of
int8 -> quantize_int8(SparseArray, Scale, 0);
int4 -> quantize_int4(SparseArray, Scale, 0)
end,
{ok, {QuantizedSparse, Mask}}.
structured_prune_quantize(Array, PruningRatio, QuantMethod, Options) ->
% Structured pruning followed by quantization
% 1. Structured pruning (e.g., channel-wise)
{ok, PrunedArray} = structured_prune(Array, PruningRatio, Options),
% 2. Quantization of remaining weights
case QuantMethod of
int8 ->
{ok, Scale} = compute_symmetric_scale(PrunedArray, 8),
quantize_int8(PrunedArray, Scale, 0);
int4 ->
{ok, Scale} = compute_symmetric_scale(PrunedArray, 4),
quantize_int4(PrunedArray, Scale, 0)
end.
%% Model compression
compress_model(Model, CompressionConfig) ->
% Comprehensive model compression
#{
quantization := QuantConfig,
pruning := PruneConfig,
distillation := DistillConfig
} = CompressionConfig,
% 1. Quantization
{ok, QuantizedModel} = apply_quantization(Model, QuantConfig),
% 2. Pruning
{ok, PrunedModel} = apply_pruning(QuantizedModel, PruneConfig),
% 3. Knowledge distillation (if teacher model provided)
case maps:get(teacher_model, DistillConfig, undefined) of
undefined -> {ok, PrunedModel};
TeacherModel -> apply_distillation(PrunedModel, TeacherModel, DistillConfig)
end.
decompress_model(CompressedModel) ->
% Decompress model for inference
#{
quantized_weights := QuantWeights,
quantization_params := QuantParams,
pruning_masks := PruneMasks
} = CompressedModel,
% Dequantize weights
DequantizedWeights = maps:map(fun(LayerName, QuantWeight) ->
#{scale := Scale, zero_point := ZeroPoint} = maps:get(LayerName, QuantParams),
{ok, Dequantized} = dequantize(QuantWeight, Scale, ZeroPoint),
Dequantized
end, QuantWeights),
% Apply pruning masks
FinalWeights = maps:map(fun(LayerName, Weight) ->
case maps:get(LayerName, PruneMasks, undefined) of
undefined -> Weight;
Mask ->
{ok, Pruned} = mlx:multiply(Weight, Mask),
Pruned
end
end, DequantizedWeights),
{ok, FinalWeights}.
estimate_compression_ratio(OriginalModel, CompressedModel) ->
% Estimate compression ratio
OriginalSize = estimate_model_size(OriginalModel),
CompressedSize = estimate_model_size(CompressedModel),
CompressionRatio = OriginalSize / CompressedSize,
#{
original_size => OriginalSize,
compressed_size => CompressedSize,
compression_ratio => CompressionRatio,
space_savings => (1.0 - CompressedSize / OriginalSize) * 100
}.
%% Helper functions
quantize_uint8(Array, Scale, ZeroPoint) ->
{ok, Scaled} = mlx:divide(Array, Scale),
{ok, Shifted} = mlx:add(Scaled, ZeroPoint),
{ok, Rounded} = mlx:round(Shifted),
{ok, Clamped} = mlx:clip(Rounded, mlx:array(0), mlx:array(255)),
mlx:cast(Clamped, uint8).
compute_symmetric_scale(Weight, NumBits) ->
{ok, MaxVal} = mlx:max(mlx:abs(Weight)),
QMax = (1 bsl (NumBits - 1)) - 1,
mlx:divide(MaxVal, mlx:array(QMax)).
gptq_quantize(Weights, Options) ->
% GPTQ quantization algorithm (simplified)
% In practice, this would require Hessian computation and iterative quantization
BlockSize = maps:get(block_size, Options, 128),
maps:map(fun(_Name, Weight) ->
{ok, Scale} = compute_symmetric_scale(Weight, 4),
quantize_int4(Weight, Scale, 0)
end, Weights).
awq_quantize(Weights, Options) ->
% AWQ (Activation-aware Weight Quantization) - simplified
ActivationStats = maps:get(activation_stats, Options, #{}),
maps:map(fun(LayerName, Weight) ->
SaliencyScore = maps:get(LayerName, ActivationStats, 1.0),
AdjustedScale = compute_awq_scale(Weight, SaliencyScore),
quantize_int4(Weight, AdjustedScale, 0)
end, Weights).
compute_awq_scale(Weight, SaliencyScore) ->
{ok, BaseScale} = compute_symmetric_scale(Weight, 4),
{ok, AdjustedScale} = mlx:multiply(BaseScale, mlx:array(SaliencyScore)),
{ok, AdjustedScale}.
collect_activation_stats(Stats) ->
% Collect activation statistics during calibration
% This would hook into the computation graph
Stats.
get_layer_id(Array) ->
% Generate layer ID for tracking
erlang:phash2(Array).
structured_prune(Array, PruningRatio, Options) ->
% Structured pruning implementation
PruningType = maps:get(type, Options, channel),
case PruningType of
channel -> channel_prune(Array, PruningRatio);
block -> block_prune(Array, PruningRatio, Options)
end.
channel_prune(Array, PruningRatio) ->
% Channel-wise pruning
{ok, [OutChannels, InChannels | _]} = mlx:shape(Array),
NumPruneChannels = round(OutChannels * PruningRatio),
% Compute channel importance (L2 norm)
{ok, ChannelNorms} = mlx:sqrt(mlx:sum(mlx:square(Array), [1, 2, 3])),
{ok, _, PruneIndices} = mlx:topk(ChannelNorms, NumPruneChannels, false),
% Create pruning mask
{ok, Mask} = create_channel_mask(OutChannels, PruneIndices),
mlx:multiply(Array, Mask).
block_prune(Array, PruningRatio, Options) ->
BlockSize = maps:get(block_size, Options, [4, 4]),
% Block-wise pruning implementation
Array. % Simplified
create_channel_mask(NumChannels, PruneIndices) ->
% Create channel pruning mask
mlx_nif:create_channel_mask(NumChannels, PruneIndices).
apply_quantization(Model, QuantConfig) ->
% Apply quantization to model
{ok, Model}. % Simplified
apply_pruning(Model, PruneConfig) ->
% Apply pruning to model
{ok, Model}. % Simplified
apply_distillation(StudentModel, TeacherModel, DistillConfig) ->
% Apply knowledge distillation
{ok, StudentModel}. % Simplified
estimate_model_size(Model) ->
% Estimate model size in bytes
maps:fold(fun(_Name, Weight, Acc) ->
{ok, NumElements} = mlx:size(Weight),
{ok, Dtype} = mlx:dtype(Weight),
BytesPerElement = case Dtype of
float32 -> 4;
float16 -> 2;
int8 -> 1;
int4 -> 0.5
end,
Acc + NumElements * BytesPerElement
end, 0, Model).