def quantize_per_block(
    input: torch.Tensor,
    dtype,
    group_size,
    scales=None,
    zero_points=None,
    sym_quant=False,
):
    r"""
    Quantize a weight tensor of Linear modules per block.
    Assume the tensor shape is [output channel, input channel],
    block shape is [1, group_size].

    Args:
        input: The tensor to be quantized
        dtype: data type of the quantized tensor, int8, int4 or nf4
        group_size: Size of group along input channel
        scales: Scales for quantization. If None, find by min/max.
        zero_points: zero points for quantization. If None, find by min/max.
        sym_quant: symmetric or asymmetric quantization

    Returns:
        A tuple of
        - The quantized tensor
        - Scales in shape [N, #block_k]
        - Zero points in shape [N, #block_k]
    """
    assert (
        input.dim() == 2
    ), f"{__name__}: Expect input has 2 dimensions but got {input.dim()}"
    assert group_size > 0, f"{__name__}: Expect group_size > 0 but got {group_size}"
    assert dtype in (WoqWeightDtype.INT8, WoqWeightDtype.INT4, WoqWeightDtype.NF4)
    assert not (
        dtype == WoqWeightDtype.NF4 and not sym_quant
    ), "NF4 must be symmetric quant"
    N = input.size(0)
    K = input.size(1)
    k_rem = K % group_size
    has_rem = k_rem != 0

    def get_qparams(scales, zps):
        if scales is not None and (sym_quant or zps is not None):
            return scales, zps
        eps = torch.tensor([torch.finfo(torch.float32).eps])
        t_com = input[:, : K - k_rem].view(N, K // group_size, group_size)
        mins = torch.minimum(t_com.min(dim=-1)[0], torch.tensor([0]))
        maxs = torch.maximum(t_com.max(dim=-1)[0], torch.tensor([0]))
        zps = None
        if dtype == WoqWeightDtype.INT8:
            if sym_quant:
                scales = torch.maximum(torch.abs(maxs), torch.abs(mins)) / 127
                scales = torch.max(scales, eps)
            else:
                scales = (maxs - mins) / 255
                scales = torch.max(scales, eps)
                zps = -torch.round(mins / scales)
                zps -= 128
        elif dtype == WoqWeightDtype.INT4:
            if sym_quant:
                scales = torch.maximum(torch.abs(maxs), torch.abs(mins)) / 7
                scales = torch.max(scales, eps)
            else:
                scales = (maxs - mins) / 15
                scales = torch.max(scales, eps)
                zps = -torch.round(mins / scales)
        else:  # NF4
            scales = torch.maximum(torch.abs(maxs), torch.abs(mins))
            scales = torch.max(scales, eps)
        if k_rem != 0:
            t_rem = input[:, K - k_rem :].view(N, 1, k_rem)
            mins_rem = torch.minimum(t_rem.min(dim=-1)[0], torch.tensor([0]))
            maxs_rem = torch.maximum(t_rem.max(dim=-1)[0], torch.tensor([0]))
            zps_rem = None
            if dtype == WoqWeightDtype.INT8:
                if sym_quant:
                    scales_rem = (
                        torch.maximum(torch.abs(maxs_rem), torch.abs(mins_rem)) / 127
                    )
                    scales_rem = torch.max(scales_rem, eps)
                else:
                    scales_rem = (maxs_rem - mins_rem) / 255
                    scales_rem = torch.max(scales_rem, eps)
                    zps_rem = -torch.round(mins_rem / scales_rem)
                    zps_rem -= 128
            elif dtype == WoqWeightDtype.INT4:
                if sym_quant:
                    scales_rem = (
                        torch.maximum(torch.abs(maxs_rem), torch.abs(mins_rem)) / 7
                    )
                    scales_rem = torch.max(scales_rem, eps)
                else:
                    scales_rem = (maxs_rem - mins_rem) / 15
                    scales_rem = torch.max(scales_rem, eps)
                    zps_rem = -torch.round(mins_rem / scales_rem)
            else:  # NF4
                scales_rem = torch.maximum(torch.abs(maxs_rem), torch.abs(mins_rem))
                scales_rem = torch.max(scales_rem, eps)
            scales = torch.cat([scales, scales_rem], dim=-1)
            if not sym_quant:
                assert zps is not None and zps_rem is not None
                zps = torch.cat([zps, zps_rem], dim=-1)
        return scales, zps

    scales, zps = get_qparams(scales, zero_points)
    Kc = (K + group_size - 1) // group_size
    t_com = input[:, : K - k_rem].view(N, K // group_size, group_size)
    scales_com = scales[:, : Kc - has_rem]
    inv_scales_com = 1 / scales_com.unsqueeze(-1)
    if not sym_quant:
        assert zps is not None
        zps_com = zps[:, : Kc - has_rem]
    else:
        zps_com = None
    if dtype == WoqWeightDtype.INT8:
        qmin = -128
        qmax = 127
        qt = torch.clamp(
            torch.round(t_com * inv_scales_com)
            + (zps_com.unsqueeze(-1) if zps_com is not None else 0),
            min=qmin,
            max=qmax,
        )
    elif dtype == WoqWeightDtype.INT4:
        qmin = 0
        qmax = 15
        # for sym_quant, shift to 0-15 for storage
        qt = torch.clamp(
            torch.round(t_com * inv_scales_com)
            + (zps_com.unsqueeze(-1) if zps_com is not None else 8),
            min=qmin,
            max=qmax,
        )
    else:  # NF4
        qt = map_float_tensor_to_nf4(t_com * inv_scales_com)
    qt = qt.view(N, K // group_size * group_size)
    if k_rem != 0:
        t_rem = input[:, K - k_rem :].view(N, 1, k_rem)
        scales_rem = scales[:, Kc - has_rem :]
        inv_scales_rem = 1 / scales_rem.unsqueeze(-1)
        if not sym_quant:
            assert zps is not None
            zps_rem = zps[:, Kc - has_rem :]
        else:
            zps_rem = None
        if dtype == WoqWeightDtype.INT8:
            qt_rem = torch.clamp(
                torch.round(t_rem * inv_scales_rem)
                + (zps_rem.unsqueeze(-1) if zps_rem is not None else 0),
                min=qmin,
                max=qmax,
            )
        elif dtype == WoqWeightDtype.INT4:
            # for sym_quant, shift to 0-15 for storage
            qt_rem = torch.clamp(
                torch.round(t_rem * inv_scales_rem)
                + (zps_rem.unsqueeze(-1) if zps_rem is not None else 8),
                min=qmin,
                max=qmax,
            )
        else:  # NF4
            qt_rem = map_float_tensor_to_nf4(t_rem * inv_scales_rem)
        qt_rem = qt_rem.view(N, k_rem)
        qt = torch.cat([qt, qt_rem], dim=1).contiguous()
    # INT8 weight: always store in int8
    # INT4 weight: store in int8 if sym_quant, otherwise in uint8
    # NF4 weight: always store in uint8
    qt = qt.to(torch.uint8 if is_4bit(dtype) else torch.int8)
    qt = qt.view(N, K)
    if is_4bit(dtype):
        if qt.size(-1) % 2:
            qt = torch.nn.functional.pad(qt, (0, 1), value=0)
        qt = qt[:, 1::2].bitwise_left_shift(4).bitwise_or_(qt[:, ::2])
    return qt.contiguous(), scales, zps

    Quantize a weight tensor of Linear modules per block.
    Assume the tensor shape is [output channel, input channel],
    block shape is [1, group_size].

    Args:
        input: The tensor to be quantized
        dtype: data type of the quantized tensor, int8, int4 or nf4
        group_size: Size of group along input channel
        scales: Scales for quantization. If None, find by min/max.
        zero_points: zero points for quantization. If None, find by min/max.
        sym_quant: symmetric or asymmetric quantization

    Returns:
        A tuple of
        - The quantized tensor
        - Scales in shape [N, #block_k]
        - Zero points in shape [N, #block_k]
    
intel_extension_for_pytorch/quantization/_quantize_utils.py