def quantized_embedding_4bit(context, node):
    """Lower the 4-bit quantized embedding op used in executorch."""
    inputs = _get_inputs(context, node, expected=[6, 7])
    weight = inputs[0].val
    weight_scales = inputs[1].val
    weight_zero_points = None
    if inputs[2] is not None and inputs[2].val is not None:
        weight_zero_points = inputs[2].val
    weight_quant_min = inputs[3].val
    weight_quant_max = inputs[4].val
    indices = inputs[5]

    out_np_dtype = None
    if len(inputs) > 6:
        if isinstance(inputs[6].val, _torch.dtype):
            out_np_dtype = NUM_TO_NUMPY_DTYPE[TORCH_DTYPE_TO_NUM[inputs[6].val]]
        elif isinstance(inputs[6].val, (int, _np.generic)):
            out_np_dtype = NUM_TO_NUMPY_DTYPE[inputs[6].val]
    if out_np_dtype is not None:
        weight_scales = weight_scales.astype(out_np_dtype)

    if weight_quant_min == 0 and weight_quant_max == 0:
        # Executorch wrongly passes both weight_quant_min and weight_quant_max. We should set it to correct numbers.
        signed = True
        weight_quant_min = -8
        weight_quant_max = 7
    else:
        signed = weight_quant_min < 0

    quant_low = -8 if signed else 0
    quant_high = 7 if signed else 15
    quant_torch_dtype = _torch.int8 if signed else _torch.uint8
    if weight_quant_min != quant_low:
        raise ValueError(
            f"The weight_quant_min should be {quant_low} for 4-bit embedding, but got {weight_quant_min}."
        )
    if weight_quant_max != quant_high:
        raise ValueError(
            f"The weight_quant_max should be {quant_high} for 4-bit embedding, but got {weight_quant_max}."
        )

    # Unpack the weight to the normal layout.
    with _torch.no_grad():
        weight = _torch.from_numpy(weight)
        # The original weight was packed by using 8-bit to represent two numbers, so we need to separate them.
        help_move_bits = 2**4
        weight_even = weight.div(help_move_bits, rounding_mode="trunc")
        weight_odd = weight.remainder(help_move_bits)
        weight_unpacked = _torch.stack((weight_even, weight_odd), dim=-1)
        weight = weight_unpacked.view(weight.shape[0], -1)
        weight = weight.view(quant_torch_dtype).add(weight_quant_min).numpy()

    if not _np.logical_and(weight >= quant_low, weight <= quant_high).all():
        raise ValueError(
            f"All elements in weight should be within 4-bit range ({quant_low} to {quant_high})."
        )

    quantized_np_dtype = types.nptype_from_builtin(
        types.string_to_builtin("int4" if signed else "uint4")
    )
    dequant_weight = _utils._construct_constexpr_dequant_op(
        weight.astype(quantized_np_dtype),
        weight_zero_points,
        weight_scales,
        axis=-1,
        name=inputs[0].name,
    )

    gather = mb.gather(x=dequant_weight, indices=indices, name=node.name)
    context.add(gather)
Lower the 4-bit quantized embedding op used in executorch.
coremltools/converters/mil/frontend/torch/quantization_ops.py