본문 바로가기
WIKI 기술 지식 베이스

Faiss scalar quantization

원문 보기 위키 갱신

출처: 문서

본문

Faiss 스칼라 양자화

OpenSearch는 Faiss 엔진을 위한 내장 스칼라 양자화를 지원해요. Faiss 스칼라 양자기는 수집 과정에서 32비트 부동소수점 입력 벡터를 더 낮은 비트의 표현으로 변환하고, 양자화된 벡터를 벡터 인덱스에 저장해요. OpenSearch는 1비트, 2비트, 4비트, 16비트 Faiss 스칼라 양자화를 지원해요.

양자화는 recall(재현율)이 다소 손실되는 대신 메모리 사용량을 줄일 수 있어요. SIMD 최적화와 함께 사용하면 Faiss 스칼라 양자화는 검색 지연 시간을 크게 줄이고 색인 처리량을 높일 수도 있어요.

Windows에서는 SIMD 최적화가 지원되지 않아요. Windows에서 Faiss 스칼라 양자화를 사용하면 색인 처리량 감소와 검색 지연 시간 증가를 포함해 성능이 크게 저하될 수 있어요. {: .warning}

Faiss 스칼라 양자화 사용 방법

Faiss 스칼라 양자화를 사용하려면 벡터 인덱스를 생성할 때 k-NN 벡터 필드의 method.parameters.encoder.name을 sq로 설정하세요. method.parameters.encoder.parameters 객체에 bits 파라미터를 반드시 지정해야 해요:

PUT /test-index
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector1": {
        "type": "knn_vector",
        "dimension": 3,
        "space_type": "l2",
        "method": {
          "name": "hnsw",
          "engine": "faiss",
          "parameters": {
            "encoder": {
              "name": "sq",
              "parameters": {
                "bits": 16
              }
            },
            "ef_construction": 256,
            "m": 8
          }
        }
      }
    }
  }
}

{% include copy-curl.html %}

Faiss sq 인코더는 다음 파라미터를 지원해요.

파라미터 이름 필수 여부 기본값 설명
bits 예 없음 각 벡터 차원을 양자화하는 데 사용되는 비트 수예요. 유효한 값은 1, 2, 4, 16이에요.
type 아니요 fp16 사용할 스칼라 양자화 타입이에요. 유효한 값은 fp16과 bf16이에요. fp16 인코더의 경우 벡터 값이 [-65504.0, 65504.0] 범위 안에 있어야 해요. bf16 인코더는 모든 유한한 32비트 부동소수점 값을 받아들여요.
clip 아니요 false fp16의 경우 true이면 지원 범위를 벗어난 벡터 값이 범위 안에 들어오도록 반올림돼요. false이면 벡터 값 중 하나라도 지원 범위를 벗어날 때 요청이 거절돼요. clip을 true로 설정하면 recall이 감소할 수 있어요. bf16의 경우 clip을 true로 설정하면 요청이 거절되고, false로 설정하면 아무 효과가 없어요.

type과 clip 파라미터는 16비트 양자화에서만 지원돼요. bits를 다른 값으로 설정한 상태에서 type이나 clip을 지정하면 요청이 거절돼요. {: .warning}

1비트, 2비트, 4비트 양자화

가장 적은 메모리 사용을 위해 각 벡터 차원을 1, 2, 4비트로 양자화하세요. 각 비트 폭은 compression_level에 대응돼요.

비트 compression_level 32비트 벡터 대비 메모리 감소 도입 버전
1 32x 32x 3.6
2 16x 16x 3.9
4 8x 8x 3.9

차원당 비트 수가 적을수록 recall을 희생하는 대신 더 작은 인덱스가 만들어져요. 이 비트 폭은 HNSW 방식에서만 지원되며, IVF는 16비트 양자화가 필요해요. 1비트 양자화는 메모리 최적화 검색을 사용해요.

다음 예제는 knn_vector 매핑에서 compression_level을 16x로 설정해 float 필드에 2비트 양자화를 활성화해요. 1비트 또는 4비트 양자화를 사용하려면 compression_level을 32x 또는 8x로 설정하세요:

PUT /test-index
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector1": {
        "type": "knn_vector",
        "dimension": 8,
        "space_type": "l2",
        "compression_level": "16x"
      }
    }
  }
}

{% include copy-curl.html %}

또는 sq 인코더에 bits를 설정해 인코더를 명시적으로 지정할 수도 있어요:

PUT /test-index
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector1": {
        "type": "knn_vector",
        "dimension": 8,
        "space_type": "l2",
        "method": {
          "name": "hnsw",
          "engine": "faiss",
          "parameters": {
            "encoder": {
              "name": "sq",
              "parameters": {
                "bits": 2
              }
            },
            "ef_construction": 256,
            "m": 8
          }
        }
      }
    }
  }
}

{% include copy-curl.html %}

1비트 양자화는 half_float 벡터에서도 지원돼요. half_float 필드는 method 매핑에서 encoder를 받아들이지 않기 때문에, 대신 compression_level을 16x로 설정하세요. 이 레벨은 half_float 벡터의 16비트 기준으로 측정되기 때문에 각 차원을 1비트에 매핑해요.

16비트 양자화

16비트 양자화에서는 Faiss 스칼라 양자기가 32비트 부동소수점 벡터를 16비트 벡터로 변환해 벡터 인덱스에 저장해요. 검색 시에는 저장된 16비트 값이 거리 계산을 위해 다시 32비트 부동소수점 값으로 변환돼요. Intel Sapphire Rapids 이상 세대 프로세서에서는 OpenSearch가 16비트 값으로 직접 거리를 계산하는데, bf16 내적에는 AVX-512 BF16 명령어를, fp16 코사인 유사도에는 AVX-512 FP16 명령어를 사용해요. 자세한 내용은 SIMD 최적화를 참고하세요.

OpenSearch는 type 파라미터로 지정하는 두 가지 16비트 인코더 타입을 지원해요:

  • fp16 (기본값): 지수 5비트와 가수 10비트를 사용하는 IEEE 754 half-precision 포맷(FP16)이에요. 이 포맷은 16비트 중 가장 높은 정밀도를 제공하지만 값 범위는 [-65504.0, 65504.0]으로 더 좁아요. 벡터 값이 모두 이 범위 안에 속한다면 fp16을 사용하세요.
  • bf16: 지수 8비트와 가수 7비트를 사용하는 bfloat16 포맷(BF16)이에요. 32비트 부동소수점 수와 동일한 값 범위를 커버하지만 정밀도는 더 낮아서 recall이 약간 더 크게 감소할 수 있어요. 벡터에 fp16 범위를 벗어난 값이 포함될 수 있거나 fp16 범위 검증과 클리핑을 피하고 싶다면 bf16을 사용하세요.

두 인코더 타입 모두 벡터 차원당 2바이트를 저장하기 때문에, recall 손실을 최소화하면서 메모리 사용량을 절반으로 줄일 수 있어요.

fp16 인코더

fp16 인코더는 32비트 벡터를 16비트 벡터로 변환해요. 이 인코더 타입에서는 벡터 값이 [-65504.0, 65504.0] 범위 안에 있어야 해요. 범위를 벗어난 값을 처리하는 방법을 정의하려면 clip 파라미터를 지정할 수 있어요. 이 파라미터의 기본값은 false이며, 범위를 벗어난 값을 포함하는 벡터는 모두 거절돼요.

clip이 true로 설정되면 범위를 벗어난 벡터 값이 지원 범위 안에 들어오도록 올림 또는 내림돼요. 예를 들어 원본 32비트 벡터가 [65510.82, -65504.1]이면 이 벡터는 16비트 벡터 [65504.0, -65504.0]으로 색인돼요.

지원 범위를 벗어나는 벡터 차원이 아주 적은 경우에만 clip을 true로 설정하는 것을 권장해요. 값을 반올림하면 recall이 감소할 수 있어요. {: .note}

다음 예제는 16비트 양자화와 함께 Faiss fp16 인코더를 지정해요. 이 인코더는 범위를 벗어난 벡터 값이 포함된 색인 요청을 모두 거절해요(clip 파라미터의 기본값이 false이기 때문이에요):

PUT /test-index
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector1": {
        "type": "knn_vector",
        "dimension": 3,
        "space_type": "l2",
        "method": {
          "name": "hnsw",
          "engine": "faiss",
          "parameters": {
            "encoder": {
              "name": "sq",
              "parameters": {
                "bits": 16
              }
            },
            "ef_construction": 256,
            "m": 8
          }
        }
      }
    }
  }
}

{% include copy-curl.html %}

벡터를 색인할 때는 각 벡터 차원이 지원 범위 안에 있는지 확인하세요:

PUT test-index/_doc/1
{
  "my_vector1": [-65504.0, 65503.845, 55.82]
}

{% include copy-curl.html %}

벡터를 쿼리할 때는 쿼리 벡터에 범위 제한이 없어요:

GET test-index/_search
{
  "size": 2,
  "query": {
    "knn": {
      "my_vector1": {
        "vector": [265436.876, -120906.256, 99.84],
        "k": 2
      }
    }
  }
}

{% include copy-curl.html %}

bf16 인코더

3.9에 도입됨 {: .label .label-purple }

bf16 인코더는 32비트 벡터를 bfloat16 벡터로 변환해요. bfloat16은 32비트 부동소수점 수와 동일한 수의 지수 비트를 사용하기 때문에 동일한 값 범위를 커버해요. 따라서 모든 유한한 32비트 부동소수점 값은 범위 기반 거절이나 클리핑 없이 색인될 수 있어요. 양자화 과정에서 각 값은 표현 가능한 가장 가까운 bfloat16 값으로 반올림되며, 가수는 23비트에서 7비트로 줄어들어요. 그 결과 bf16은 fp16에 비해 정밀도를 희생하는 대신 더 넓은 범위를 제공해요.

다음 예제는 16비트 양자화와 함께 Faiss bf16 인코더를 지정해요:

PUT /test-index-bf16
{
  "settings": {
    "index": {
      "knn": true
    }
  },
  "mappings": {
    "properties": {
      "my_vector1": {
        "type": "knn_vector",
        "dimension": 3,
        "space_type": "l2",
        "method": {
          "name": "hnsw",
          "engine": "faiss",
          "parameters": {
            "encoder": {
              "name": "sq",
              "parameters": {
                "bits": 16,
                "type": "bf16"
              }
            },
            "ef_construction": 256,
            "m": 8
          }
        }
      }
    }
  }
}

{% include copy-curl.html %}

벡터 값이 특정 범위로 제한되지 않으므로 어떤 차원도 거절되지 않아요:

PUT test-index-bf16/_doc/1
{
  "my_vector1": [-150000.5, 123456.75, 55.82]
}

{% include copy-curl.html %}

유한한 값만 허용돼요. NaN이나 무한대 값이 포함된 색인 요청은 거절돼요. {: .note}

bf16 인코더에는 다음과 같은 제약 사항이 있어요:

  • clip을 true로 설정하는 것은 지원되지 않으며 요청이 거절돼요. bf16은 32비트 부동소수점 값 범위 전체를 커버하기 때문에 클리핑은 아무 효과가 없어요.
  • 원격 인덱스 빌드는 지원되지 않아요. bf16 인코더를 사용하는 인덱스는 항상 로컬에서 빌드돼요.

메모리 추정

가장 좋은 경우, 양자화된 벡터는 32비트 벡터에 필요한 메모리의 다음 비율만큼만 필요해요.

비트 32비트 벡터 메모리 대비 비율 감소 배율
1 3.125% 32x
2 6.25% 16x
4 12.5% 8x
16 50% 2x

HNSW 메모리 추정

HNSW(Hierarchical Navigable Small Worlds)에 필요한 메모리는 벡터당 1.1 * (dimension * bits_per_dimension / 8 + 8 * m)바이트로 추정해요. 여기서 m은 그래프를 구성하는 동안 각 요소에 대해 생성되는 최대 양방향 링크 수예요.

예를 들어 차원이 256이고 m이 16인 100만 개의 벡터가 있다고 가정해 보세요. 각 비트 폭에 필요한 메모리는 다음과 같이 추정할 수 있어요.

비트 추정식 결과
1 1.1 * (256 * 1 / 8 + 8 * 16) * 1,000,000 ~0.176 GB
2 1.1 * (256 * 2 / 8 + 8 * 16) * 1,000,000 ~0.211 GB
4 1.1 * (256 * 4 / 8 + 8 * 16) * 1,000,000 ~0.282 GB
16 1.1 * (256 * 16 / 8 + 8 * 16) * 1,000,000 ~0.656 GB

IVF 메모리 추정

IVF에 필요한 메모리는 1.1 * (((bytes_per_dimension * dimension) * num_vectors) + (4 * nlist * dimension))바이트로 추정해요. 여기서 nlist는 벡터를 분할할 버킷 수예요.

예를 들어 차원이 256이고 nlist가 128인 100만 개의 벡터가 있다고 가정해 보세요.

IVF는 16비트 Faiss 스칼라 양자화에서만 지원돼요. 1비트, 2비트, 4비트 양자화는 HNSW 방식에서만 지원돼요. {: .note}

16비트 양자화의 경우 필요한 메모리는 다음과 같이 추정할 수 있어요:

1.1 * (((2 * 256) * 1,000,000) + (4 * 128 * 256))  ~= 0.525 GB

다음 단계

더 알아보기 (Learn more)