Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions client/qiskit_serverless/core/clients/serverless_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,6 @@ def jobs(self, function: Optional[QiskitFunction] = None, **kwargs) -> List[Job]
job.get("id"),
job_service=self,
raw_data=job,
compute_profile=job.get("compute_profile"),
)
for job in response_data.get("results", [])
]
Expand Down Expand Up @@ -291,7 +290,6 @@ def provider_jobs(self, function: Optional[QiskitFunction], **kwargs) -> List[Jo
job.get("id"),
job_service=self,
raw_data=job,
compute_profile=job.get("compute_profile"),
)
for job in response_data.get("results", [])
]
Expand All @@ -314,7 +312,6 @@ def job(self, job_id: str) -> Optional[Job]:
job = Job(
job_id=job_id,
job_service=self,
compute_profile=response_data.get("compute_profile"),
)

return job
Expand Down Expand Up @@ -376,7 +373,6 @@ def run(
return Job(
job_id,
job_service=self,
compute_profile=response_data.get("compute_profile"),
)

def get_job_data(self, job_id: str) -> Optional[dict]:
Expand Down
2 changes: 0 additions & 2 deletions client/qiskit_serverless/core/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,6 @@ def __init__(
job_id: str,
job_service: JobService,
raw_data: Optional[Dict[str, Any]] = None,
compute_profile: Optional[str] = None,
):
"""Job class for async script execution.

Expand All @@ -178,7 +177,6 @@ def __init__(
"""
self.job_id = job_id
self._job_service = job_service
self.compute_profile = compute_profile
self.raw_data = raw_data or {}

@property
Expand Down
12 changes: 3 additions & 9 deletions client/tests/core/test_serverless_client_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1140,16 +1140,13 @@ def test_run_with_compute_profile(self, mock_client):
json=mock_response,
)

job = mock_client.run(program="test-program", compute_profile="gx3d-24x120x1a100p")
mock_client.run(program="test-program", compute_profile="gx3d-24x120x1a100p")

# Verify client sent compute_profile to API
# Verify client sent the (deprecated) compute_profile input to the API
request_data = json.loads(mock_request.last_request.text)
assert "compute_profile" in request_data
assert request_data["compute_profile"] == "gx3d-24x120x1a100p"

# Verify Job.compute_profile property works
assert job.compute_profile == "gx3d-24x120x1a100p"

def test_run_without_compute_profile(self, mock_client):
"""Test run() without compute_profile - backend applies default."""
# Mock response includes default compute_profile applied by backend
Expand All @@ -1169,11 +1166,8 @@ def test_run_without_compute_profile(self, mock_client):
json=mock_response,
)

job = mock_client.run(program="test-program")
mock_client.run(program="test-program")

# Verify client sent compute_profile as None (backend will apply default)
request_data = json.loads(mock_request.last_request.text)
assert request_data["compute_profile"] is None

# Verify backend applied the default
assert job.compute_profile == "cx3d-4x16"
9 changes: 6 additions & 3 deletions gateway/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,6 @@ class JobAdmin(admin.ModelAdmin):
"fields": [
"filler",
"fleet_id",
"compute_profile",
"compute_profile_fk",
"size_source",
"function_size",
Expand Down Expand Up @@ -575,7 +574,11 @@ def job_timeline_view(self, request):
continue
# one single query: the rendering iterates the jobs again, and re-running the queryset
# could come back empty (deleted in between) and break the rendering half way through
jobs = list(Job.objects.filter(id__in=id_list).select_related("author").prefetch_related("job_events"))
jobs = list(
Job.objects.filter(id__in=id_list)
.select_related("author", "compute_profile_fk")
.prefetch_related("job_events")
)
if not id_list or not jobs:
messages.error(request, "No jobs selected for the timeline.")
return redirect(reverse("admin:api_job_changelist"))
Expand Down Expand Up @@ -725,7 +728,7 @@ def index(self, request, extra_context=None):
if timeline_context is None:
recent_jobs = list(
Job.objects.filter(runner=Program.FLEETS)
.select_related("author")
.select_related("author", "compute_profile_fk")
.prefetch_related("job_events")
.order_by("-created")[:20]
)
Expand Down
2 changes: 1 addition & 1 deletion gateway/api/domain/job_timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def _jobs_from_queryset(jobs_qs):
"status": job.status,
"runner": job.runner,
"filler": job.filler,
"profile": job.compute_profile or "-",
"profile": job.compute_profile_id or "-",
"created": job.created,
"updated": job.updated,
"running_started_at": job.running_started_at,
Expand Down
37 changes: 37 additions & 0 deletions gateway/api/migrations/0064_remove_job_compute_profile_state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("api", "0063_merge_20260902_1720"),
]

operations = [
# Step 1 of a two-step removal of Job.compute_profile (the transitional
# string). compute_profile_fk (FK -> ComputeProfile) is the source of
# truth; readers in this repo now go through Job.compute_profile_id, which
# reads the FK. Here the field is dropped from Django's state ONLY; the
# api_job.compute_profile column is left in place so the previous release
# -- whose Job model still declares the field and lists it in every SELECT
# -- stays deployable. The real DROP COLUMN happens in a later release,
# once no release that declares the field can still be deployed (mirrors
# the Program.default_compute_profile removal, migrations 0057 -> 0058).
#
# BLOCKED: this migration must NOT ship until the billing team repoints
# KafkaEventStreamsClient._build_classical_metric_type
# (gateway/core/ibm_cloud/event_streams/kafka_event_streams_client.py) off
# job.compute_profile and onto job.compute_profile_fk.compute_profile_id.
# That file is owned by another team and is deliberately untouched here;
# removing the field from the model state while it still reads the string
# attribute would raise AttributeError when a usage event is built.
migrations.SeparateDatabaseAndState(
state_operations=[
migrations.RemoveField(
model_name="job",
name="compute_profile",
),
],
database_operations=[],
),
]
12 changes: 5 additions & 7 deletions gateway/api/use_cases/programs/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,11 @@ def _get_runner_config(
) -> RunnerConfig:
"""Resolve the compute profile and sizing provenance for a run.

``compute_profile`` (string) is transitional and will be removed; the FK
``compute_profile_fk`` is the source of truth going forward. They always agree
at creation. A Fleets job always resolves to a profile string; if no
``ComputeProfile`` row is registered for it, that is a deployment
misconfiguration and we reject the job rather than store a null FK. Ray leaves
``compute_profile`` None (profiles are a Fleets concept), so the FK stays null.
``compute_profile_fk`` is the source of truth for the profile a job runs at
and is what gets stored on the job. A Fleets job always resolves to a profile;
if no ``ComputeProfile`` row is registered for it, that is a deployment
misconfiguration and we reject the job rather than store a null FK. Ray is not
profiled (profiles are a Fleets concept), so its FK stays null.

Because the size determines the compute profile (and not the reverse -- two
sizes can map to one profile), the returned :class:`RunnerConfig` also records
Expand Down Expand Up @@ -237,7 +236,6 @@ def execute( # pylint: disable=too-many-locals, too-many-branches
author=user,
gpu=runner_config.gpu,
runner=function.runner,
compute_profile=runner_config.compute_profile,
compute_profile_fk=runner_config.compute_profile_fk,
size_source=runner_config.size_source,
function_size=runner_config.function_size,
Expand Down
2 changes: 1 addition & 1 deletion gateway/api/v1/views/jobs/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class JobSerializerWithoutResult(serializers.ModelSerializer):

class Meta:
model = Job
fields = ["id", "status", "program", "created", "sub_status", "compute_profile", "compute_profile_fk"]
fields = ["id", "status", "program", "created", "sub_status", "compute_profile_fk"]
ref_name = "JobsListWithoutResultInputSerializer"


Expand Down
2 changes: 0 additions & 2 deletions gateway/api/v1/views/jobs/retrieve.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ class Meta:
"created",
"sub_status",
"fleet_id",
"compute_profile",
"compute_profile_fk",
"business_model",
]
Expand Down Expand Up @@ -113,7 +112,6 @@ class Meta:
"created",
"sub_status",
"fleet_id",
"compute_profile",
"compute_profile_fk",
"business_model",
]
Expand Down
4 changes: 2 additions & 2 deletions gateway/api/v1/views/programs/get_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class OutputSerializer(serializers.ModelSerializer):

class Meta:
model = Job
fields = ["id", "result", "status", "program", "created", "sub_status", "fleet_id", "compute_profile"]
fields = ["id", "result", "status", "program", "created", "sub_status", "fleet_id"]
ref_name = "ProgramsGetJobsOutput"


Expand All @@ -65,7 +65,7 @@ class OutputSerializerWithoutResult(serializers.ModelSerializer):

class Meta:
model = Job
fields = ["id", "status", "program", "created", "sub_status", "fleet_id", "compute_profile"]
fields = ["id", "status", "program", "created", "sub_status", "fleet_id"]
ref_name = "ProgramsGetJobsOutputWithoutResult"


Expand Down
4 changes: 1 addition & 3 deletions gateway/api/v1/views/programs/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,9 @@ class Meta:
class OutputSerializer(serializers.ModelSerializer):
"""Response serializer for a queued job."""

compute_profile = serializers.CharField(required=False, allow_null=True, allow_blank=True, default=None)

class Meta:
model = Job
fields = ["id", "result", "status", "program", "created", "arguments", "compute_profile", "size_source"]
fields = ["id", "result", "status", "program", "created", "arguments", "size_source"]
ref_name = "ProgramsRunOutput"


Expand Down
16 changes: 10 additions & 6 deletions gateway/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,12 +550,6 @@ class Job(models.Model):
help_text="True when this job was created by the filler-jobs balancer to occupy idle GPU "
"capacity, instead of coming from a real user request.",
)
compute_profile = models.CharField(
max_length=255,
null=True,
blank=True,
help_text="Code Engine compute profile for Fleets runner (e.g., gx3d-24x120x1a100p)",
)
logs = models.TextField(default="No logs yet.")
runner = models.CharField(
max_length=20, choices=Program.RUNNER_CHOICES, default=Program.RAY, help_text="Execution backend: ray or fleets"
Expand Down Expand Up @@ -658,6 +652,16 @@ def in_terminal_state(self):
"""Returns true if job is in terminal state."""
return self.status in self.TERMINAL_STATUSES

@property
def compute_profile_id(self) -> str | None:
"""Bare compute-profile string from the FK (the source of truth).

Returns None for Ray jobs and any historical row with no
``compute_profile_fk``. Reads ``compute_profile_fk_id`` first so an
unset FK costs no extra query.
"""
return self.compute_profile_fk.compute_profile_id if self.compute_profile_fk_id else None

def save_direct(self, fields: list[str]) -> None:
"""Persist selected fields bypassing optimistic-locking validation.

Expand Down
10 changes: 5 additions & 5 deletions gateway/core/services/runners/fleets_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def submit(self) -> None:
logger.info(
"job_id=[%s] profile [%s] → cpu=%s memory=%s gpu=%s",
self.job.id,
self.job.compute_profile or "default",
self.job.compute_profile_id or "default",
cpu_limit,
memory_limit,
scale_gpu,
Expand Down Expand Up @@ -792,10 +792,10 @@ def _build_fleet_name(self) -> str:
"""
prefix = "fil" if self.job.filler else "job"
function = _fleet_name_segment(self.job.program.title)
# Unlike the title and the username, compute_profile is nullable, and submit() runs on
# Unlike the title and the username, the compute profile is nullable, and submit() runs on
# settings.DEFAULT_COMPUTE_PROFILE when it is unset (see _parse_compute_profile). Name the
# fleet after the profile it actually runs on rather than after the empty column.
profile = _fleet_name_segment(self.job.compute_profile or settings.DEFAULT_COMPUTE_PROFILE)
# fleet after the profile it actually runs on rather than after the empty value.
profile = _fleet_name_segment(self.job.compute_profile_id or settings.DEFAULT_COMPUTE_PROFILE)
username = _fleet_name_segment(self.job.author.username)
return f"{prefix}-{function}-{profile}-{username}-{_fleet_name_timestamp()}"

Expand All @@ -810,7 +810,7 @@ def _parse_compute_profile(self) -> tuple[str, str, dict | None]:
is the V2GPUScalePrototype dict or ``None`` when no GPU is
specified in the profile.
"""
profile = self.job.compute_profile or settings.DEFAULT_COMPUTE_PROFILE
profile = self.job.compute_profile_id or settings.DEFAULT_COMPUTE_PROFILE

# Normalize away any instance-family prefix. New jobs are already bare
# (the view normalizes at ingest), but this stays a safety net for
Expand Down
9 changes: 4 additions & 5 deletions gateway/scheduler/tasks/balance_filler_jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def _balance_filler_jobs(self, program: Program, filler_jobs: list[Job]) -> None
self._stop_filler_jobs(stale)

if len(current) < target:
self._create_filler_job(program, compute_profile)
self._create_filler_job(program)
elif len(current) > target:
self._stop_filler_jobs(current[: len(current) - target])

Expand Down Expand Up @@ -209,7 +209,7 @@ def _discard_unsubmitted_filler_jobs(self) -> None:
)
self._mark_failed(job)

def _create_filler_job(self, program: Program, compute_profile: str) -> None:
def _create_filler_job(self, program: Program) -> None:
"""Create and submit one filler job, the most this task creates per loop.

One per loop rather than the whole shortfall, so the COS upload and the Code
Expand All @@ -221,10 +221,10 @@ def _create_filler_job(self, program: Program, compute_profile: str) -> None:
# A shutdown is not a failure of the work, so it buys no delay.
if self.kill_signal.received:
return
if not self._submit_filler_job(program, compute_profile):
if not self._submit_filler_job(program):
self._retry_loops = RETRY_AFTER_LOOPS

def _submit_filler_job(self, program: Program, compute_profile: str) -> bool:
def _submit_filler_job(self, program: Program) -> bool:
"""Create and submit one filler job. True when it reached PENDING."""
project = program.code_engine_project
job = Job(
Expand All @@ -234,7 +234,6 @@ def _submit_filler_job(self, program: Program, compute_profile: str) -> bool:
author=program.author,
filler=True,
runner=Program.FLEETS,
compute_profile=compute_profile,
compute_profile_fk=program.default_size.compute_profile,
size_source=Job.SIZE_SOURCE_NONE,
function_size=program.default_size,
Expand Down
Loading