Skip to content

Fix issue #367 – System message not supported for Anthropic (anthropic.BadRequestError) - #368

Merged
Ying1123 merged 3 commits into
sgl-project:mainfrom
fronx:main
Apr 16, 2024
Merged

Ying1123 merged 3 commits into
sgl-project:mainfrom
fronx:main

Conversation

@fronx

@fronx fronx commented Apr 15, 2024

Copy link
Copy Markdown
Contributor

This fixes #367 by converting a system prompt provided as a message to Anthropic's special system parameter.

It is assumed that a system prompt is only ever passed as the first message. After extracting it, the first message is removed from the message list.

fronx and others added 3 commits April 15, 2024 22:48
This makes it possible to run tests under test/lang/ like so:

  python -m unittest test.lang.test_anthropic_backend
…dling

Using the system role with the Anthropic backend resulted in the following error:

anthropic.BadRequestError: Error code: 400 - {
    'type': 'error',
    'error': {
        'type': 'invalid_request_error',
        'message': 'messages: Unexpected role "system". The Messages API accepts a top-level `system` parameter, not "system" as an input message role.'
    }
}

The fix converts system prompts passed as a message to
Anthropic's special `system` parameter while keeping the
interface compatible with other LLM backends.
Comment thread python/sglang/backend/anthropic.py
@Ying1123
Ying1123 merged commit 2b6d999 into sgl-project:main Apr 16, 2024
@Ying1123

Copy link
Copy Markdown
Contributor

@fronx Thanks for the contribution! It is merged.

timethink pushed a commit to timethink/sglang that referenced this pull request Mar 9, 2025
…c (anthropic.BadRequestError) (sgl-project#368)

Co-authored-by: Ying Sheng <sqy1415@gmail.com>
shiyu7 pushed a commit to shiyu7/sglang that referenced this pull request Dec 26, 2025
efschu added a commit to efschu/htsglang that referenced this pull request Aug 1, 2026
…sgl-project#366)

sgl-project#369 released the bar1 graph gate, so sgl-project#366 could finally measure. It still
could not measure with CUDA graphs, and the reason is the result that matters
most here.

WHY EAGER. bar1 + CUDA graphs + NEXTN does not cold-capture in a usable time on
the 27B FP8 vehicle. One graph boot ran >35 min without finishing capture, all
three cards pinned at 100% util the whole time -- forward progress, not a
wedge. The log says why, dozens of times: "Using default W8A8 Block FP8 kernel
config ... Config file not found ... device_name=NVIDIA_GeForce_RTX_5090,
dtype=fp8_w8a8, block_shape=[128,128]". The rig ships no tuned W8A8 block-fp8
configs for the 5090, so every GEMM shape falls into a cold triton autotune
with ~3 min between shapes. That is the sgl-project#255/sgl-project#368 tuned-config gap, and warming
those configs once would make every future bar1 graph boot fast -- a concrete,
cheap follow-up. Running EAGER skips capture, costs ~34 s per boot, and puts
both transports in the SAME regime, so the transport question is answered
honestly. The table therefore does NOT quote sgl-project#354's graph-mode NCCL baseline;
it measures NCCL itself, this window, eager.

s17_bar1_eager_arm.sh boots one arm from the htsglang image on the PVE host and
is parameterised by transport. Its gate is two-sided and mandatory per boot:
a bar1 arm must log ACHIEVED=bar1 on every barlink group (world/tp/dcp x 3
ranks) and an NCCL arm must log ZERO barlink lines, so neither a silent
fallback nor an accidental barlink run can enter the table. All 8 arms passed.

Three environment defects were found and fixed inside it, each documented at
the point of the fix:
  * TORCH_CUDA_ARCH_LIST in the image made the JIT extension build as
    _cuda_75_80_86_89_90_100_120, missing the warm _86_120 cache and
    cold-compiling seven arches. Pinned to '8.6;12.0' -> cache hit in 0.2 s.
  * The INT8 fork sgl-kernel wheel is built against CUDA 12.9 and the image is
    cu13, so installing it BROKE sgl_kernel outright (libcublas.so.12, then
    libcudart.so.12 missing -> common_ops unimportable -> every INT8 rank died
    with "needs sgl_kernel.int8_scaled_mm, which this build has no code for").
    Fixed by mounting a directory of symlinks to every *.so.12* the CT999 venv
    ships and APPENDING it to the image's own LD_LIBRARY_PATH -- the SONAMEs
    differ from .so.13, so torch keeps its cu13 libs.
  * A single quote inside the wheel-inject string closed the docker -c '...'
    wrapper and killed INT8 boots in 2-3 s before the container existed.

s17_bar1_eager_table.py renders the deliverable: both levers separately, never
only the diagonal, with sgl-project#354's noise floors carried and any delta under its row
floor printed as "within noise". Decode reads the client-side token rate for
every arm: under eager + docker-logs buffering the server-log tick parser sees
zero ticks, so one consistent source beats a mixed column.

Test results (2026-08-01, Qwen3.6-27B TP=3 uneven 5090+2x3080, NEXTN 3/1/4,
KV fp8_e4m3, ctx 32768, EAGER, 8 boots, all gates passed):

  point         FP8 NCCL  FP8 bar1  d(tr)      INT8 NCCL  INT8 bar1  d(tr)
  prefill_s1      1335.8    1395.8  +4.5%         1412.1     1687.5  +19.5%
  prefill_s8      1329.0    1454.6  +9.5%         1448.2     1674.5  +15.6%
  decode_bs1        25.4      36.0  +41.7%          35.4       33.8  -4.5%
  decode_bs8       217.8     219.7  within noise   230.2      260.3  +13.1%

  Transport lever: bar1 wins prefill in BOTH formats (FP8 +4.5/+9.5%,
  INT8 +19.5/+15.6%) and is mixed on decode.
  Format lever: INT8 > FP8 on prefill under both transports
  (NCCL +5.7/+9.0%, bar1 +20.9/+15.1%).

Nothing under python/ was touched, so no repo test suite is affected.
ruff, codespell and bash -n clean.
efschu added a commit to efschu/htsglang that referenced this pull request Aug 5, 2026
… chain in one engine

The fold is now a first-class arm, not a footnote. Folding w_q x w_scale into
bf16/fp16 weights at BUILD time removes the constraint that shaped the INT8
design: with no runtime dequantize there is no activation quantization either,
so TensorRT's inability to express a per-token dynamic scale stops mattering.
The INT8 arms are per-stage engine islands with our quant kernel between them;
folded, chain_decode_layer is ONE engine containing four GEMMs, the SiLU-gate
multiply and the attention-core bridge, with nothing of ours inside it.

22 more engines built at the desk, CPU-only AOT, real checkpoint weights, 19 s
for all 32. Variants: int8 (per-stage), fold_bf16, fold_fp16 (whole chain),
plus one fp32_ref shape per rank as a labelled diagnostic.

QUALITY, MEASURED RATHER THAN ASSERTED. The fold does not coarsen anything --
w_q x w_scale is the checkpoint's weight exactly, in a wider container -- and it
removes the per-token activation quantization error the deployed path pays. So
the harness computes an exact fp32 reference from the dequantized weights and
gates each fold on being AT LEAST AS ACCURATE AS WHAT WE DEPLOY, rather than on
a fixed tolerance. Gating a fold against the deployed OUTPUT would have measured
the deployed path's own quantization error and called it a fold defect. Mock,
random weights, directional: deployed 0.0112, fold bf16 0.0031, fold fp16
0.0024, max-abs vs exact.

THE MOCK FOUND AN fp16 LIMIT. fp16 wins on mantissa (11 bits vs bf16's 8) and
loses on range (5 exponent bits, max 65504; bf16 carries fp32's range). In the
4-GEMM chain_decode_layer the intermediates compound and the fp16 fold produced
a NON-FINITE output where bf16 did not. Random weights exaggerate the
magnitudes, so this is a flag for the card run and not a verdict -- but it
qualifies the "2^-11 beats the INT8 grid" rationale exactly: the deep chains are
where range, not mantissa, decides. Non-finite output is detected per arm and
recorded as a result, never averaged into a number.

THE STAIR IS THE POINT, and the memory cost is now priced per part. Every fold
engine measures 1.999x the INT8 weight bytes; full-model fold does not fit the
3080s and was never the proposal. Per-part cost is in the manifest as
weight_bytes_fold / weight_bytes_int8 / memory_multiplier_vs_int8 for every
engine, and tabulated in TARGET_SELECTION, so the planner can price residency
for any subset of the stair. Hot parts folded, everything else stays INT8 and
stays offloadable.

CROSSOVER, measured under graphs on both sides: the fold trades zero
activation-quant kernels against 2x the weight bytes moved in a memory-bound
GEMV. sgl-project#368's 78 % quant share was an EAGER number and graph replay already cuts
it to ~11 %, so the fold is buying back a smaller slice than the eager figure
suggests. New verdict cells fold_bf16_over_torch_graph,
fold_fp16_over_torch_graph and fold_bf16_over_trt_int8 across M = 1,2,4,8; the
last one separates "the fold helps" from "the engine helps", which are different
wins and a per-part decision needs both.

The fp32 arm is one shape, labelled DIAGNOSTIC_fp32_ref_over_torch_graph, and is
disqualified as a deployment candidate by memory at 4 bytes per weight. It
isolates TensorRT runtime quality from the quantization constraints and nothing
else.

Assumptions independently checked against the fork and the checkpoint before
building: every language-model linear is bias=False with no .bias key in the
checkpoint; every module in the target set is INT8-quantized (only lm_head,
embed_tokens, in_proj_a/b, visual and mtp are ignored); and silu applies to the
FIRST half times the second (activation.py:123-125), which is the epilogue the
engines emit.

Tests: mock_smoke.sh green with the fold arms in the rotation -- 10 points, 52
arm measurements, tolerance AND quality paths exercised on every point, fp16
chain overflow surfaced as a NOTE and not swallowed. 32 real engines built with
checkpoint weights. ruff clean, codespell clean. NOTHING executed on a GPU: the
cards were held by another window throughout and every step ran with
CUDA_VISIBLE_DEVICES="".
efschu added a commit to efschu/htsglang that referenced this pull request Aug 17, 2026
… as data, and the embed-quant checks

sgl-project#725. THE BRIEF'S PREMISE DOES NOT HOLD, and saying so is the finding. The
task was to locate where the cost model prices quantized-linear lanes as
M-independent or with a wrong crossover, and to stop bs=1-decode points being
priced through the quant stage. Neither happens: the planner has NO
activation-quant axis at all. decode is weight_bytes / effective_decode_bw
(key_solver.decode_weight_time), prefill is 2*params / gemm_tflops
(key_solver.prefill_compute_time), and quantization enters those two formulas
only as a smaller weight-byte count and as which lane rate is picked (sgl-project#324). A
T=2 decode point never passes through a quant stage, so there is no misprice
to make red. Per the brief's own escape clause this reduces to
data-with-provenance plus pins.

TWO CORRECTIONS TO THE FRAMING, both load-bearing:

1. The real dispatch decision is not in the planner. It is
   layers/quantization/fp8_dequant_gemv.py:324 (fused_gemv_applicable, aspect
   test N >= K) and the sm120 NVFP4 buckets in nvfp4_scaled_mm_sm120.cuh:
   192-201 -- per-call runtime branches.

2. sgl-project#368 measured the small-M quant penalty as largely a LAUNCH CONSTANT that
   CUDA-graph replay removes: int8_quant 0.0266 -> 0.0012 ms (~21x), quant
   share of the fused op 61% -> 11%. NInfer's thresholds are dispatch branches
   in a C++ header, i.e. taken per call in an eager engine, and a captured
   graph cannot branch on T at replay. Wiring an eager-measured crossover into
   our graph-captured decode path would import an artefact. The table is
   therefore recorded with NO consumer, deliberately (sgl-project#421's audit class: an
   instrument that looks wired and is not is worse than one honestly inert).

WHAT THE HARVEST IS WORTH TODAY: corroboration, and it is real. Two codebases,
two mechanisms, two GPU classes agree on which family must not take the
small-M quant/fused path -- the K>N shapes. Our own sm86 aspect gate separates
exactly those ([5120,6144] 0.86x LOSE, [5120,17408] 0.90x LOSE). Five of six
shapes agree in direction.

(d) sm86 stays EMPTY. NInfer measured sm120a only, and sgl-project#368 established sm86
is GEMM-slow rather than quant-dominant, so its crossovers are not merely
unmeasured but expected to differ in KIND. An estimate would be a number with
no measurement wearing the same type as five that have one.

sgl-project#724, checks only, no port. Checkpoint read by headers only (120 and 2624
bytes of header JSON; no shard mmapped, no tensor data touched).

(a) YES on all three counts, from two independent places. config.json:
tie_word_embeddings False, and quantization_config.ignore names 'lm_head' and
're:.*embed_tokens.*'. Safetensors headers confirm it without trusting the
config: lm_head.weight BF16 [248320, 5120], embed_tokens.weight BF16
[248320, 5120].

  This CLOSES the one open divergence from sgl-project#725. That shape is exactly
  NInfer's lm_head row -- the single shape where their "A16 always" points
  opposite to our N>=K gate (N/K=48.5, selects fused). On this checkpoint
  lm_head is BF16, so no activation-quant gate ever sees it: moot.

(b) NO gap in that form; the kernel exists and IS wired, just scoped.
  main model  qwen3_5.py:1386-1405 builds embed_tokens quantized-resident via
    GGUFEmbeddingMethod (gguf.py:1532), gated explicitly to GGUF; every other
    quantization keeps quant_config=None and takes the dense path.
  NEXTN draft qwen3_5_mtp.py:185-204 set_embed_and_head_modules hands the
    draft the target's MODULE OBJECTS rather than tensors -- precisely because
    a packed qweight has no .weight to hand over -- so the draft inherits
    whatever the main model built and has no independent path to carry a gap.

  The only real delta is a capability question for its own decision: extending
  quantized-resident vocab beyond GGUF would require re-quantizing embed and
  lm_head, which this checkpoint's producer deliberately excluded, so the
  ~1.1 GiB/rank prize is not available without changing the checkpoint.

Tests, hermetic (CUDA_VISIBLE_DEVICES="", no GPU, no model load, no serving):
  test_activation_quant_crossover_725.py  13 passed + 31 subtests
    MUTATION PROOF: estimating an sm86 row and making ABSENT answer False
    turns 3 pins red
  test/registered/unit/planner  2524 passed, 123 skipped, 188 subtests,
    2 failed -- PROVEN PRE-EXISTING by re-running the failing file with both
    new files moved aside: same 2 fail (test_rejected_evidence_pins.py).
  ruff + codespell clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

System message not supported for Anthropic (anthropic.BadRequestError)

2 participants