Repository: huggingface/text-generation-inference

Issue:
Qwen1.5/Qwen2 model additions
### Model description

[Qwen2 models](https://huggingface.co/Qwen/Qwen1.5-72B-Chat) architecture has been added to `transformers` [version 4.37.0](https://github.com/huggingface/transformers/releases/tag/v4.37.0). Would be useful to be able to serve those models in TGI.

### Open source status

- [X] The model implementation is available
- [X] The model weights are available

### Provide useful links for the implementation

* [transformers pull request](https://github.com/huggingface/transformers/pull/28436)
* [vllm implementation of the qwen2 architecture](https://github.com/vllm-project/vllm/pull/2495)


Discussion:

Resolving patch:
diff --git a/server/text_generation_server/models/__init__.py b/server/text_generation_server/models/__init__.py
--- a/server/text_generation_server/models/__init__.py
+++ b/server/text_generation_server/models/__init__.py
@@ -54,6 +54,9 @@
     from text_generation_server.models.flash_llama import (
         FlashLlama,
     )
+    from text_generation_server.models.flash_qwen2 import (
+        FlashQwen2,
+    )
     from text_generation_server.models.flash_gemma import (
         FlashGemma,
     )
@@ -81,6 +84,7 @@
     __all__.append(FlashMistral)
     __all__.append(FlashMixtral)
     __all__.append(FlashPhi)
+    __all__.append(FlashQwen2)
     __all__.append(FlashStarcoder2)
 
 MAMBA_AVAILABLE = True
@@ -328,6 +332,27 @@ def get_model(
                 dtype=dtype,
                 trust_remote_code=trust_remote_code,
             )
+    elif model_type == "qwen2":
+        if FLASH_ATTENTION:
+            return FlashQwen2(
+                    model_id,
+                    revision,
+                    quantize=quantize,
+                    dtype=dtype,
+                    trust_remote_code=trust_remote_code,
+            )
+        elif sharded:
+            raise NotImplementedError(
+                FLASH_ATT_ERROR_MESSAGE.format("Sharded Qwen2")
+            )
+        else:
+            return CausalLM(
+                model_id,
+                revision,
+                quantize=quantize,
+                dtype=dtype,
+                trust_remote_code=trust_remote_code,
+            )
     if model_type == "gemma":
         if FLASH_ATTENTION:
             return FlashGemma(
diff --git a/server/text_generation_server/models/custom_modeling/flash_qwen2_modeling.py b/server/text_generation_server/models/custom_modeling/flash_qwen2_modeling.py
new file mode 100644
--- /dev/null
+++ b/server/text_generation_server/models/custom_modeling/flash_qwen2_modeling.py
@@ -0,0 +1,390 @@
+import torch
+import torch.distributed
+
+from torch import nn
+from transformers.activations import ACT2FN
+from typing import Optional, List, Tuple
+
+from text_generation_server.utils import paged_attention, flash_attn
+from text_generation_server.utils.layers import (
+    TensorParallelRowLinear,
+    TensorParallelColumnLinear,
+    TensorParallelEmbedding,
+    PositionRotaryEmbedding,
+    TensorParallelHead,
+    get_linear,
+    FastRMSNorm,
+)
+
+
+def load_attention(config, prefix, weights):
+    if config.num_attention_heads != config.num_key_value_heads:
+        return _load_gqa(config, prefix, weights)
+    else:
+        return TensorParallelColumnLinear.load_multi(
+            config,
+            prefixes=[f"{prefix}.q_proj", f"{prefix}.k_proj", f"{prefix}.v_proj"],
+            dim=0,
+            weights=weights,
+            bias=True,
+        )
+
+
+def _load_gqa(config, prefix: str, weights):
+    assert config.hidden_size % config.num_attention_heads == 0
+    assert config.num_attention_heads % weights.process_group.size() == 0
+
+    weight = weights.get_multi_weights_col(
+        prefixes=[f"{prefix}.q_proj", f"{prefix}.k_proj", f"{prefix}.v_proj"],
+        quantize=config.quantize,
+        dim=0,
+    )
+
+    if config.quantize not in ["gptq", "awq"]:
+        weight = weight.to(dtype=weights.dtype).to(device=weights.device)
+
+        head_size = config.hidden_size // config.num_attention_heads
+        num_heads = config.num_attention_heads // weights.process_group.size()
+        num_key_value_heads = config.num_key_value_heads // weights.process_group.size()
+        assert list(weight.shape) == [
+            (num_heads + 2 * num_key_value_heads) * head_size,
+            config.hidden_size,
+        ], f"{list(weight.shape)} != {[(num_heads + 2 * config.num_key_value_heads) * head_size, config.hidden_size]}"
+
+    return TensorParallelColumnLinear(
+        get_linear(weight, bias=None, quantize=config.quantize)
+    )
+
+
+class Qwen2Attention(torch.nn.Module):
+    def __init__(
+        self,
+        prefix: str,
+        config,
+        weights,
+    ):
+        super().__init__()
+        self.max_past = (
+            config.sliding_window if config.sliding_window is not None else -1
+        )
+        self.num_heads = config.num_attention_heads
+        self.hidden_size = config.hidden_size
+        self.head_size = self.hidden_size // self.num_heads
+
+        self.rotary_emb = PositionRotaryEmbedding.static(
+            config=config,
+            dim=self.head_size,
+            base=config.rope_theta,
+            device=weights.device,
+        )
+
+        self.softmax_scale = self.head_size**-0.5
+
+        if self.num_heads % weights.process_group.size() != 0:
+            raise ValueError(
+                f"`num_heads` must be divisible by `num_shards` (got `num_heads`: {self.num_heads} "
+                f"and `num_shards`: {weights.process_group.size()}"
+            )
+        self.num_heads = self.num_heads // weights.process_group.size()
+        self.num_key_value_heads = (
+            config.num_key_value_heads // weights.process_group.size()
+        )
+
+        self.query_key_value = load_attention(config, prefix, weights)
+
+        self.o_proj = TensorParallelRowLinear.load(
+            config,
+            prefix=f"{prefix}.o_proj",
+            weights=weights,
+            bias=False,
+        )
+        self.num_groups = self.num_heads // self.num_key_value_heads
+        self.kv_head_mapping = torch.arange(
+            0, self.num_key_value_heads, dtype=torch.int32, device=weights.device
+        ).repeat_interleave(self.num_groups)
+
+    def forward(
+        self,
+        hidden_states,
+        cos,
+        sin,
+        cu_seqlen_prefill,
+        kv_cache,
+        block_tables,
+        slots,
+        input_lengths,
+        max_s,
+        prefill_cache_indices,
+    ):
+        qkv = self.query_key_value(hidden_states)
+        query, kv = qkv.split(
+            [
+                self.head_size * self.num_heads,
+                2 * self.head_size * self.num_key_value_heads,
+            ],
+            dim=1,
+        )
+        query = query.view(-1, self.num_heads, self.head_size)
+        kv = kv.view(-1, 2, self.num_key_value_heads, self.head_size)
+
+        self.rotary_emb(query, torch.select(kv, dim=1, index=0), cos, sin)
+
+        if prefill_cache_indices is not None:
+            kv_to_cache = kv[prefill_cache_indices]
+        else:
+            kv_to_cache = kv
+
+        paged_attention.reshape_and_cache(
+            kv_to_cache[:, 0], kv_to_cache[:, 1], kv_cache[0], kv_cache[1], slots
+        )
+
+        # output tensor
+        attn_output = torch.empty_like(query)
+
+        # Prefill
+        if cu_seqlen_prefill is not None:
+            # flash attention
+            flash_attn.attention(
+                query,
+                torch.select(kv, dim=1, index=0),
+                torch.select(kv, dim=1, index=1),
+                attn_output,
+                cu_seqlen_prefill,
+                max_s,
+                self.softmax_scale,
+                window_size_left=self.max_past,
+            )
+        # Decode
+        else:
+            paged_attention.attention(
+                attn_output,
+                query,
+                kv_cache[0],
+                kv_cache[1],
+                self.kv_head_mapping,
+                self.softmax_scale,
+                block_tables,
+                input_lengths,
+                max_s,
+            )
+
+        return self.o_proj(attn_output.view(-1, self.num_heads * self.head_size))
+
+class Qwen2MLP(nn.Module):
+    def __init__(self, prefix, config, weights):
+        super().__init__()
+        act = config.hidden_act
+        self.act = (
+            ACT2FN[act]
+            if "gelu" not in act
+            else lambda x: torch.nn.functional.gelu(
+                x,
+                approximate=(
+                    "tanh" if act in ["gelu_fast", "gelu_pytorch_tanh"] else "none"
+                ),
+            )
+        )
+        # Fuse gate and up proj
+        self.gate_up_proj = TensorParallelColumnLinear.load_multi(
+            config,
+            prefixes=[f"{prefix}.gate_proj", f"{prefix}.up_proj"],
+            weights=weights,
+            dim=0,
+            bias=False,
+        )
+        self.down_proj = TensorParallelRowLinear.load(
+            config,
+            prefix=f"{prefix}.down_proj",
+            weights=weights,
+            bias=False,
+        )
+        self.intermediate_size = (
+            config.intermediate_size // weights.process_group.size()
+        )
+
+    def forward(self, hidden_states):
+        gate_up_states = self.gate_up_proj(hidden_states)
+        gate_up_states = gate_up_states.view(-1, 2, self.intermediate_size)
+        return self.down_proj(self.act(gate_up_states[:, 0]) * gate_up_states[:, 1])
+
+
+class Qwen2Layer(nn.Module):
+    def __init__(self, layer_id, config, weights):
+        super().__init__()
+        prefix = f"model.layers.{layer_id}"
+        self.self_attn = Qwen2Attention(prefix=f"{prefix}.self_attn", config=config, weights=weights)
+        self.mlp = Qwen2MLP(prefix=f"{prefix}.mlp", config=config, weights=weights)
+        self.input_layernorm = FastRMSNorm.load(
+            prefix=f"{prefix}.input_layernorm", weights=weights, eps=config.rms_norm_eps
+        )
+        self.post_attention_layernorm = FastRMSNorm.load(
+            prefix=f"{prefix}.post_attention_layernorm",
+            weights=weights,
+            eps=config.rms_norm_eps,
+        )
+
+    def forward(
+        self,
+        hidden_states,
+        residual,
+        cos,
+        sin,
+        cu_seqlen_prefill,
+        kv_cache,
+        block_tables,
+        slots,
+        input_lengths,
+        max_s,
+        prefill_cache_indices,
+    ):
+        normed_hidden_states, res = self.input_layernorm(hidden_states, residual)
+
+        # Self Attention
+        attn_output = self.self_attn(
+            normed_hidden_states,
+            cos,
+            sin,
+            cu_seqlen_prefill,
+            kv_cache,
+            block_tables,
+            slots,
+            input_lengths,
+            max_s,
+            prefill_cache_indices,
+        )
+
+        # faster post attention rms norm
+        normed_attn_res_output, attn_res = self.post_attention_layernorm(
+            attn_output, res
+        )
+
+        mlp_output = self.mlp(normed_attn_res_output)
+
+        return mlp_output, attn_res
+
+class Qwen2Model(torch.nn.Module):
+    def __init__(self, config, weights):
+        super().__init__()
+        process_group = weights.process_group
+        self.tp_rank = process_group.rank()
+        self.tp_world_size = process_group.size()
+        self.embed_tokens = TensorParallelEmbedding(
+            prefix="model.embed_tokens", weights=weights
+        )
+        self.layers = nn.ModuleList(
+            [
+                Qwen2Layer(
+                    layer_id,
+                    config,
+                    weights,
+                )
+                for layer_id in range(config.num_hidden_layers)
+            ]
+        )
+        self.norm = FastRMSNorm.load(
+            prefix="model.norm", weights=weights, eps=config.rms_norm_eps
+        )
+
+        self.gradient_checkpointing = False
+        
+        self.head_size = self.layers[0].self_attn.head_size
+        self.num_heads = self.layers[0].self_attn.num_heads
+        self.num_key_value_heads = self.layers[0].self_attn.num_key_value_heads
+
+    def forward(
+        self,
+        input_ids: torch.Tensor,
+        position_ids: torch.Tensor,
+        cu_seqlen_prefill: Optional[torch.Tensor],
+        kv_cache: List[Tuple[torch.Tensor, torch.Tensor]],
+        block_tables: torch.Tensor,
+        slots: torch.Tensor,
+        input_lengths: torch.Tensor,
+        max_s: int,
+        true_max_s: int,
+        prefill_cache_indices: Optional[torch.Tensor],
+    ) -> torch.Tensor:
+        hidden_states = self.embed_tokens(input_ids)
+
+        # Get rotary cos and sin for this forward
+        # Avoid to index in each layer
+        cos, sin = self.layers[0].self_attn.rotary_emb.get_cos_sin(
+            position_ids, true_max_s, hidden_states.dtype
+        )
+
+        residual = None
+        for i, layer in enumerate(self.layers):
+            hidden_states, residual = layer(
+                hidden_states,
+                residual,
+                cos,
+                sin,
+                cu_seqlen_prefill,
+                kv_cache[i],
+                block_tables,
+                slots,
+                input_lengths,
+                max_s,
+                prefill_cache_indices,
+            )
+
+        hidden_states, _ = self.norm(hidden_states, residual)
+
+        return hidden_states
+
+
+class Qwen2ForCausalLM(torch.nn.Module):
+    def __init__(self, config, weights):
+        super().__init__()
+
+        self.model = Qwen2Model(config, weights)
+        self.lm_head = TensorParallelHead.load(
+            config,
+            prefix="lm_head",
+            weights=weights,
+        )
+        self.max_past = config.sliding_window
+        self.max_past_tensor = (
+            torch.tensor(config.sliding_window, device=weights.device)
+            if self.max_past is not None
+            else None
+        )
+
+    def forward(
+        self,
+        input_ids: torch.Tensor,
+        position_ids: torch.Tensor,
+        cu_seqlen_prefill: Optional[torch.Tensor],
+        kv_cache: List[Tuple[torch.Tensor, torch.Tensor]],
+        block_tables: torch.Tensor,
+        slots: torch.Tensor,
+        input_lengths: torch.Tensor,
+        max_s: int,
+        prefill_cache_indices: Optional[torch.Tensor] = None,
+        lm_head_indices: Optional[torch.Tensor] = None,
+    ) -> torch.Tensor:
+        true_max_s = max_s
+        if prefill_cache_indices is not None:
+            # Slots also need to be sliced as it has the same size as the whole kv tensor
+            slots = slots[prefill_cache_indices]
+        elif self.max_past is not None:
+            # Clamp in decode mode as paged attention requires clamped values whereas the flash attention
+            # kernel requires the true values
+            input_lengths = torch.clamp(input_lengths, max=self.max_past_tensor)
+
+        hidden_states = self.model(
+            input_ids,
+            position_ids,
+            cu_seqlen_prefill,
+            kv_cache,
+            block_tables,
+            slots,
+            input_lengths,
+            max_s,
+            true_max_s,
+            prefill_cache_indices,
+        )
+        if lm_head_indices is not None:
+            hidden_states = hidden_states[lm_head_indices]
+        logits = self.lm_head(hidden_states)
+        return logits
diff --git a/server/text_generation_server/models/flash_qwen2.py b/server/text_generation_server/models/flash_qwen2.py
new file mode 100644
--- /dev/null
+++ b/server/text_generation_server/models/flash_qwen2.py
@@ -0,0 +1,77 @@
+import torch
+import torch.distributed
+
+from opentelemetry import trace
+from transformers import AutoTokenizer
+from transformers.models.qwen2 import Qwen2Tokenizer
+from typing import Optional
+
+from text_generation_server.models import FlashCausalLM
+from text_generation_server.models.custom_modeling.flash_qwen2_modeling import (
+    Qwen2ForCausalLM,
+)
+from transformers.models.qwen2 import Qwen2Config
+from text_generation_server.utils import (
+    initialize_torch_distributed,
+    weight_files,
+    Weights,
+)
+
+tracer = trace.get_tracer(__name__)
+
+
+class FlashQwen2(FlashCausalLM):
+    def __init__(
+        self,
+        model_id: str,
+        revision: Optional[str] = None,
+        quantize: Optional[str] = None,
+        dtype: Optional[torch.dtype] = None,
+        trust_remote_code: bool = False,
+    ):
+        self.process_group, rank, world_size = initialize_torch_distributed()
+        if torch.cuda.is_available():
+            device = torch.device(f"cuda:{rank}")
+            dtype = torch.float16 if dtype is None else dtype
+        else:
+            raise NotImplementedError("FlashQwen2 is only available on GPU")
+
+        try:
+            tokenizer = Qwen2Tokenizer.from_pretrained(
+                model_id,
+                revision=revision,
+                trust_remote_code=trust_remote_code,
+            )
+        except Exception:
+            tokenizer = AutoTokenizer.from_pretrained(
+                model_id,
+                revision=revision,
+                trust_remote_code=trust_remote_code,
+            )
+
+        config = Qwen2Config.from_pretrained(
+            model_id, revision=revision, trust_remote_code=trust_remote_code
+        )
+        config.quantize = quantize
+
+        torch.distributed.barrier(group=self.process_group)
+
+        filenames = weight_files(model_id, revision=revision, extension=".safetensors")
+        weights = Weights(filenames, device, dtype, process_group=self.process_group)
+        if config.quantize in ["gptq", "awq"]:
+            weights._set_gptq_params(model_id, revision)
+
+        model = Qwen2ForCausalLM(config, weights)
+
+        torch.distributed.barrier(group=self.process_group)
+        super(FlashQwen2, self).__init__(
+            model=model,
+            tokenizer=tokenizer,
+            num_layers=len(model.model.layers),
+            num_kv_heads=model.model.num_key_value_heads,
+            head_size=model.model.head_size,
+            dtype=dtype,
+            device=device,
+            rank=rank,
+            world_size=world_size,
+        )


Test patch:
diff --git a/integration-tests/models/__snapshots__/test_flash_qwen2/test_flash_qwen2.json b/integration-tests/models/__snapshots__/test_flash_qwen2/test_flash_qwen2.json
new file mode 100644
--- /dev/null
+++ b/integration-tests/models/__snapshots__/test_flash_qwen2/test_flash_qwen2.json
@@ -0,0 +1,84 @@
+{
+  "details": {
+    "best_of_sequences": null,
+    "finish_reason": "length",
+    "generated_tokens": 10,
+    "prefill": [
+      {
+          "id": 2271,
+          "text": "Test",
+          "logprob": null
+      },
+      {
+          "id": 1681,
+          "text": " request",
+          "logprob": -7.0351562
+      }
+    ],
+    "seed": null,
+    "tokens": [
+      {
+          "id": 369,
+          "text": " for",
+          "logprob": -2.1914062,
+          "special": false
+      },
+      {
+          "id": 279,
+          "text": " the",
+          "logprob": -2.6210938,
+          "special": false
+      },
+      {
+          "id": 2701,
+          "text": " following",
+          "logprob": -3.6445312,
+          "special": false
+      },
+      {
+          "id": 729,
+          "text": " function",
+          "logprob": -2.9648438,
+          "special": false
+      },
+      {
+          "id": 271,
+          "text": "\n\n",
+          "logprob": -1.9111328,
+          "special": false
+      },
+      {
+          "id": 31946,
+          "text": "Inputs",
+          "logprob": -1.6855469,
+          "special": false
+      },
+      {
+          "id": 25,
+          "text": ":",
+          "logprob": -1.6093254e-05,
+          "special": false
+      },
+      {
+          "id": 707,
+          "text": " def",
+          "logprob": -0.5678711,
+          "special": false
+      },
+      {
+          "id": 1477,
+          "text": " find",
+          "logprob": -2.5917969,
+          "special": false
+      },
+      {
+          "id": 6345,
+          "text": "_max",
+          "logprob": -1.8349609,
+          "special": false
+      }
+  ],
+    "top_tokens": null
+  },
+  "generated_text": " for the following function\n\nInputs: def find_max"
+}
diff --git a/integration-tests/models/__snapshots__/test_flash_qwen2/test_flash_qwen2_all_params.json b/integration-tests/models/__snapshots__/test_flash_qwen2/test_flash_qwen2_all_params.json
new file mode 100644
--- /dev/null
+++ b/integration-tests/models/__snapshots__/test_flash_qwen2/test_flash_qwen2_all_params.json
@@ -0,0 +1,84 @@
+{
+  "details": {
+    "best_of_sequences": null,
+    "finish_reason": "length",
+    "generated_tokens": 10,
+    "prefill": [
+      {
+          "id": 2271,
+          "text": "Test",
+          "logprob": null
+      },
+      {
+          "id": 1681,
+          "text": " request",
+          "logprob": -7.0351562
+      }
+    ],
+    "seed": 0,
+    "tokens": [
+      {
+          "id": 311,
+          "text": " to",
+          "logprob": -1.4472656,
+          "special": false
+      },
+      {
+          "id": 633,
+          "text": " get",
+          "logprob": -0.4741211,
+          "special": false
+      },
+      {
+          "id": 264,
+          "text": " a",
+          "logprob": 0.0,
+          "special": false
+      },
+      {
+          "id": 1140,
+          "text": " list",
+          "logprob": 0.0,
+          "special": false
+      },
+      {
+          "id": 315,
+          "text": " of",
+          "logprob": 0.0,
+          "special": false
+      },
+      {
+          "id": 678,
+          "text": " all",
+          "logprob": 0.0,
+          "special": false
+      },
+      {
+          "id": 279,
+          "text": " the",
+          "logprob": -0.2590332,
+          "special": false
+      },
+      {
+          "id": 3847,
+          "text": " users",
+          "logprob": -0.45239258,
+          "special": false
+      },
+      {
+          "id": 304,
+          "text": " in",
+          "logprob": -0.12322998,
+          "special": false
+      },
+      {
+          "id": 419,
+          "text": " this",
+          "logprob": -1.7275391,
+          "special": false
+      }
+    ],
+    "top_tokens": null
+  },
+  "generated_text": "Test request to get a list of all the users in this"
+}
diff --git a/integration-tests/models/__snapshots__/test_flash_qwen2/test_flash_qwen2_load.json b/integration-tests/models/__snapshots__/test_flash_qwen2/test_flash_qwen2_load.json
new file mode 100644
--- /dev/null
+++ b/integration-tests/models/__snapshots__/test_flash_qwen2/test_flash_qwen2_load.json
@@ -0,0 +1,338 @@
+[
+  {
+    "details": {
+      "best_of_sequences": null,
+      "finish_reason": "length",
+      "generated_tokens": 10,
+      "prefill": [
+        {
+            "id": 2271,
+            "text": "Test",
+            "logprob": null
+        },
+        {
+            "id": 1681,
+            "text": " request",
+            "logprob": -7.0351562
+        }
+      ],
+      "seed": null,
+      "tokens": [
+        {
+            "id": 369,
+            "text": " for",
+            "logprob": -2.1914062,
+            "special": false
+        },
+        {
+            "id": 279,
+            "text": " the",
+            "logprob": -2.6210938,
+            "special": false
+        },
+        {
+            "id": 2701,
+            "text": " following",
+            "logprob": -3.6445312,
+            "special": false
+        },
+        {
+            "id": 729,
+            "text": " function",
+            "logprob": -2.9648438,
+            "special": false
+        },
+        {
+            "id": 271,
+            "text": "\n\n",
+            "logprob": -1.9111328,
+            "special": false
+        },
+        {
+            "id": 31946,
+            "text": "Inputs",
+            "logprob": -1.6855469,
+            "special": false
+        },
+        {
+            "id": 25,
+            "text": ":",
+            "logprob": -1.6093254e-05,
+            "special": false
+        },
+        {
+            "id": 707,
+            "text": " def",
+            "logprob": -0.5678711,
+            "special": false
+        },
+        {
+            "id": 1477,
+            "text": " find",
+            "logprob": -2.5917969,
+            "special": false
+        },
+        {
+            "id": 6345,
+            "text": "_max",
+            "logprob": -1.8349609,
+            "special": false
+        }
+      ],
+      "top_tokens": null
+    },
+    "generated_text": " for the following function\n\nInputs: def find_max"
+  },
+  {
+    "details": {
+      "best_of_sequences": null,
+      "finish_reason": "length",
+      "generated_tokens": 10,
+      "prefill": [
+        {
+            "id": 2271,
+            "text": "Test",
+            "logprob": null
+        },
+        {
+            "id": 1681,
+            "text": " request",
+            "logprob": -7.0351562
+        }
+      ],
+      "seed": null,
+      "tokens": [
+        {
+            "id": 369,
+            "text": " for",
+            "logprob": -2.1914062,
+            "special": false
+        },
+        {
+            "id": 279,
+            "text": " the",
+            "logprob": -2.6210938,
+            "special": false
+        },
+        {
+            "id": 2701,
+            "text": " following",
+            "logprob": -3.6445312,
+            "special": false
+        },
+        {
+            "id": 729,
+            "text": " function",
+            "logprob": -2.9648438,
+            "special": false
+        },
+        {
+            "id": 271,
+            "text": "\n\n",
+            "logprob": -1.9111328,
+            "special": false
+        },
+        {
+            "id": 31946,
+            "text": "Inputs",
+            "logprob": -1.6855469,
+            "special": false
+        },
+        {
+            "id": 25,
+            "text": ":",
+            "logprob": -1.6093254e-05,
+            "special": false
+        },
+        {
+            "id": 707,
+            "text": " def",
+            "logprob": -0.5678711,
+            "special": false
+        },
+        {
+            "id": 1477,
+            "text": " find",
+            "logprob": -2.5917969,
+            "special": false
+        },
+        {
+            "id": 6345,
+            "text": "_max",
+            "logprob": -1.8349609,
+            "special": false
+        }
+      ],
+      "top_tokens": null
+    },
+    "generated_text": " for the following function\n\nInputs: def find_max"
+  },
+  {
+    "details": {
+      "best_of_sequences": null,
+      "finish_reason": "length",
+      "generated_tokens": 10,
+      "prefill": [
+        {
+            "id": 2271,
+            "text": "Test",
+            "logprob": null
+        },
+        {
+            "id": 1681,
+            "text": " request",
+            "logprob": -7.0351562
+        }
+      ],
+      "seed": null,
+      "tokens": [
+        {
+            "id": 369,
+            "text": " for",
+            "logprob": -2.1914062,
+            "special": false
+        },
+        {
+            "id": 279,
+            "text": " the",
+            "logprob": -2.6210938,
+            "special": false
+        },
+        {
+            "id": 2701,
+            "text": " following",
+            "logprob": -3.6445312,
+            "special": false
+        },
+        {
+            "id": 729,
+            "text": " function",
+            "logprob": -2.9648438,
+            "special": false
+        },
+        {
+            "id": 271,
+            "text": "\n\n",
+            "logprob": -1.9111328,
+            "special": false
+        },
+        {
+            "id": 31946,
+            "text": "Inputs",
+            "logprob": -1.6855469,
+            "special": false
+        },
+        {
+            "id": 25,
+            "text": ":",
+            "logprob": -1.6093254e-05,
+            "special": false
+        },
+        {
+            "id": 707,
+            "text": " def",
+            "logprob": -0.5678711,
+            "special": false
+        },
+        {
+            "id": 1477,
+            "text": " find",
+            "logprob": -2.5917969,
+            "special": false
+        },
+        {
+            "id": 6345,
+            "text": "_max",
+            "logprob": -1.8349609,
+            "special": false
+        }
+      ],
+      "top_tokens": null
+    },
+    "generated_text": " for the following function\n\nInputs: def find_max"
+  },
+  {
+    "details": {
+      "best_of_sequences": null,
+      "finish_reason": "length",
+      "generated_tokens": 10,
+      "prefill": [
+        {
+            "id": 2271,
+            "text": "Test",
+            "logprob": null
+        },
+        {
+            "id": 1681,
+            "text": " request",
+            "logprob": -7.0351562
+        }
+      ],
+      "seed": null,
+      "tokens": [
+        {
+            "id": 369,
+            "text": " for",
+            "logprob": -2.1914062,
+            "special": false
+        },
+        {
+            "id": 279,
+            "text": " the",
+            "logprob": -2.6210938,
+            "special": false
+        },
+        {
+            "id": 2701,
+            "text": " following",
+            "logprob": -3.6445312,
+            "special": false
+        },
+        {
+            "id": 729,
+            "text": " function",
+            "logprob": -2.9648438,
+            "special": false
+        },
+        {
+            "id": 271,
+            "text": "\n\n",
+            "logprob": -1.9111328,
+            "special": false
+        },
+        {
+            "id": 31946,
+            "text": "Inputs",
+            "logprob": -1.6855469,
+            "special": false
+        },
+        {
+            "id": 25,
+            "text": ":",
+            "logprob": -1.6093254e-05,
+            "special": false
+        },
+        {
+            "id": 707,
+            "text": " def",
+            "logprob": -0.5678711,
+            "special": false
+        },
+        {
+            "id": 1477,
+            "text": " find",
+            "logprob": -2.5917969,
+            "special": false
+        },
+        {
+            "id": 6345,
+            "text": "_max",
+            "logprob": -1.8349609,
+            "special": false
+        }
+      ],
+      "top_tokens": null
+    },
+    "generated_text": " for the following function\n\nInputs: def find_max"
+  }
+]
diff --git a/integration-tests/models/test_flash_qwen2.py b/integration-tests/models/test_flash_qwen2.py
new file mode 100644
--- /dev/null
+++ b/integration-tests/models/test_flash_qwen2.py
@@ -0,0 +1,61 @@
+import pytest
+
+
+@pytest.fixture(scope="module")
+def flash_qwen2_handle(launcher):
+    with launcher("Qwen/Qwen1.5-7B") as handle:
+        yield handle
+
+
+@pytest.fixture(scope="module")
+async def flash_qwen2(flash_qwen2_handle):
+    await flash_qwen2_handle.health(300)
+    return flash_qwen2_handle.client
+
+
+@pytest.mark.asyncio
+async def test_flash_qwen2(flash_qwen2, response_snapshot):
+    response = await flash_qwen2.generate(
+        "Test request", max_new_tokens=10, decoder_input_details=True
+    )
+
+    assert response.details.generated_tokens == 10
+    assert response.generated_text == " for the following function\n\nInputs: def find_max"
+    assert response == response_snapshot
+
+
+@pytest.mark.asyncio
+async def test_flash_qwen2_all_params(flash_qwen2, response_snapshot):
+    response = await flash_qwen2.generate(
+        "Test request",
+        max_new_tokens=10,
+        repetition_penalty=1.2,
+        return_full_text=True,
+        stop_sequences=["test"],
+        temperature=0.5,
+        top_p=0.9,
+        top_k=10,
+        truncate=5,
+        typical_p=0.9,
+        watermark=True,
+        decoder_input_details=True,
+        seed=0,
+    )
+
+    assert response.details.generated_tokens == 10
+    assert response == response_snapshot
+
+
+@pytest.mark.asyncio
+async def test_flash_qwen2_load(flash_qwen2, generate_load, response_snapshot):
+    responses = await generate_load(
+        flash_qwen2, "Test request", max_new_tokens=10, n=4
+    )
+
+    assert len(responses) == 4
+    assert all(
+        [r.generated_text == responses[0].generated_text for r in responses]
+    ), f"{[r.generated_text  for r in responses]}"
+    assert responses[0].generated_text == ": Let n = 10 - 1"
+
+    assert responses == response_snapshot