Skip to content

Commit c6d052b

Browse files
committed
parallel -> concurrent, document on fail in concurrency
1 parent 224dddc commit c6d052b

File tree

2 files changed

+131
-8
lines changed

2 files changed

+131
-8
lines changed

docs/concepts/parallelization.md renamed to docs/concepts/concurrency.md

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Parallelization
1+
# Concurrency
22
## And the Orchestration of Guard Executions
33

44
This document is a description of the current implementation of the Guardrails' validation loop. It attempts to explain the current patterns used with some notes on why those patterns were accepted at the time of implementation and potential future optimizations. It is _not_ meant to be prescriptive as there can, and will, be improvements made in future versions.
55

66
In general you will find that our approach to performance is two fold:
77
1. Complete computationally cheaper, static checks first and exit early to avoid spending time and resources on more expensive checks that are unlikely to pass when the former fail.
8-
2. Parallelize processing where possible.
8+
2. Run processes concurrently where possible.
99

1010
## Background: The Validation Loop
1111
When a Guard is executed, that is called via `guard()`, `guard.parse()`, `guard.validate()`, etc., it goes through an internal process that has the following steps:
@@ -60,7 +60,7 @@ Besides handling asynchronous calls to the LLM, using an `AsyncGuard` also ensur
6060
* An asyncio event loop is available.
6161
* The asyncio event loop is not taken/already running.
6262
63-
## Validation Orchestration and Parallelization
63+
## Validation Orchestration and Concurrency
6464
6565
### Structured Data Validation
6666
We perform validation with a "deep-first" approach. This has no meaning for unstructured text output since there is only one value, but for structured output it means that the objects are validated from the inside out.
@@ -79,7 +79,7 @@ Take the below structure as an example:
7979
}
8080
```
8181
82-
As of versions v0.4.x and v0.5.x of Guardrails, the above object would validated as follows:
82+
As of versions v0.4.x and v0.5.x of Guardrails, the above object would be validated as follows:
8383
8484
1. foo.baz
8585
2. foo.bez
@@ -88,11 +88,134 @@ As of versions v0.4.x and v0.5.x of Guardrails, the above object would validated
8888
5. bar.buz
8989
6. bar
9090
91-
> NOTE: The approach currently used, and outlined above, was predicated on the assumption that if child properties fail validation, it is unlikely that the parent property would pass. With the current atomic state of validation, it can be argued that this assumption is false. That is, the types of validations applied to parent properties typically take the form of checking the appropriate format of the container like a length check on a list. These types of checks are generally independent of any requirements the child properties have. This opens up the possibility of running all six paths listed above in parallel at once instead of performing them in steps based on key path.
91+
> NOTE: The approach currently used, and outlined above, was predicated on the assumption that if child properties fail validation, it is unlikely that the parent property would pass. With the current atomic state of validation, it can be argued that this assumption is false. That is, the types of validations applied to parent properties typically take the form of checking the appropriate format of the container like a length check on a list. These types of checks are generally independent of any requirements the child properties have. This opens up the possibility of running all six paths listed above concurrently instead of performing them in steps based on key path.
9292
9393
When synchronous validation occurs as defined in [Benefits of AsyncGuard](#benefits-of-async-guard), the validators for each property would be run in the order they are defined on the schema. That also means that any on fail actions are applied in that same order.
9494
95-
When asynchronous validation occurs, there are multiple levels of parallelization possible. First, running validation on the child properties (e.g. `foo.baz` and `foo.bez`) will happen in parallel via the asyncio event loop. Second, within the validation for each property, if the validators have `run_in_separate_process` set to `True`, they are run in parallel via multiprocessing. This multiprocessing is capped to the process count specified by the `GUARDRAILS_PROCESS_COUNT` environment variable which defaults to 10. Note that some environments, like AWS Lambda, may not support multiprocessing in which case you would need to set this environment variable to 1.
95+
When asynchronous validation occurs, there are multiple levels of concurrency possible. First, running validation on the child properties (e.g. `foo.baz` and `foo.bez`) will happen concurrently via the asyncio event loop. Second, the validators on any given property are also run concurrently via the event loop. For validators that only define a synchronous `validate` method, calls to this method are run in the event loops default executore. Note that some environments, like AWS Lambda, may not support multiprocessing in which case you would need to either set the executor to a thread processor instead or limit validation to running synchronously by setting `GUARDRAILS_PROCESS_COUNT=1` or `GUARDRAILS_RUN_SYNC=true`.
9696
9797
### Unstructured Data Validation
98-
When validating unstructured data, i.e. text, the LLM output is treated the same as if it were a property on an object. This means that the validators applied to is have the ability to run in parallel utilizing multiprocessing when `run_in_separate_process` is set to `True` on the validators.
98+
When validating unstructured data, i.e. text, the LLM output is treated the same as if it were a property on an object. This means that the validators applied to is have the ability to run concurrently utilizing the event loop.
99+
100+
### Handling Failures During Async Concurrency
101+
The Guardrails validation loop is opinionated about how it handles failures when running validators concurrently so that it spends the least amount of time processing an output that would result in a failure. It's behaviour comes down to when and what it returns based on the [corrective action](/how_to_guides/custom_validators#on-fail) specified on a validator. Corrective actions are processed concurrently since they are specific to a given validator on a given property. This means that interuptive corrective actions, namely `EXCEPTION`, will be the first corrective action enforced because the exception is raised as soon as the failure is evaluated. The remaining actions are handled in the following order after all futures are collected from the validation of a specific property:
102+
1. `FILTER` and `REFRAIN`
103+
2. `REASK`
104+
3. `FIX`
105+
106+
\*_NOTE:_ `NOOP` Does not require any special handling because it does not alter the value.
107+
108+
\*_NOTE:_ `FIX_REASK` Will fall into either the `REASK` or `FIX` bucket based on if the fixed value passes the second round of validation.
109+
110+
This means that if any valdiator with `on_fail=OnFailAction.EXCEPTION` returns a `FailResult`, then Guardrails will raise a `ValidationError` interrupting the process.
111+
112+
If any validator on a specific property which has `on_fail=OnFailAction.FILTER` or `on_fail=OnFailAction.REFRAIN` returns a `FailResult`, whichever of these is the first to finish will the returned early as the value for that property,
113+
114+
If any validator on a specific property which has `on_fail=OnFailAction.REASK` returns a `FailResult`, all reasks for that property will be merged and a `FieldReAsk` will be returned early as the value for that property.
115+
116+
If any validator on a specific property which has `on_fail=OnFailAction.FIX` returns a `FailResult`, all fix values for that property will be merged and the result of that merge will be returned as the value for that property.
117+
118+
Custom on_fail handlers will fall into one of the above actions based on what it returns; i.e. if it returns an updated value it's considered a `FIX`, if it returns an instance of `Filter` then `FILTER`, etc..
119+
120+
Let's look at an example. We'll keep the validation logic simple and write out some assertions to demonstrate the evaluation order discussed above.
121+
122+
```py
123+
import asyncio
124+
from random import randint
125+
from typing import Optional
126+
from guardrails import AsyncGuard, ValidationOutcome
127+
from guardrails.errors import ValidationError
128+
from guardrails.validators import (
129+
Validator,
130+
register_validator,
131+
ValidationResult,
132+
PassResult,
133+
FailResult
134+
)
135+
136+
@register_validator(name='custom/contains', data_type='string')
137+
class Contains(Validator):
138+
def __init__(self, match_value: str, **kwargs):
139+
super().__init__(
140+
match_value=match_value,
141+
**kwargs
142+
)
143+
self.match_value = match_value
144+
145+
def validate(self, value, metadata = {}) -> ValidationResult:
146+
if self.match_value in value:
147+
return PassResult()
148+
149+
fix_value = None
150+
if self.on_fail_descriptor == 'fix':
151+
# Insert the match_value into the value at a random index
152+
insertion = randint(0, len(value))
153+
fix_value = f"{value[:insertion]}{self.match_value}{value[insertion:]}"
154+
155+
return FailResult(
156+
error_message=f'Value must contain {self.match_value}',
157+
fix_value=fix_value
158+
)
159+
160+
exception_validator = Contains("a", on_fail='exception')
161+
filter_validator = Contains("b", on_fail='filter')
162+
refrain_validator = Contains("c", on_fail='refrain')
163+
reask_validator_1 = Contains("d", on_fail='reask')
164+
reask_validator_2 = Contains("e", on_fail='reask')
165+
fix_validator_1 = Contains("f", on_fail='fix')
166+
fix_validator_2 = Contains("g", on_fail='fix')
167+
168+
guard = AsyncGuard().use_many(
169+
exception_validator,
170+
filter_validator,
171+
refrain_validator,
172+
reask_validator_1,
173+
reask_validator_2,
174+
fix_validator_1,
175+
fix_validator_2
176+
)
177+
178+
### Trigger the exception validator ###
179+
error = None
180+
result: Optional[ValidationOutcome] = None
181+
try:
182+
result = asyncio.run(guard.validate("z", metadata={}))
183+
184+
except ValidationError as e:
185+
error = e
186+
187+
assert result is None
188+
assert error is not None
189+
assert str(error) == "Validation failed for field with errors: Value must contain a"
190+
191+
192+
193+
### Trigger the Filter and Refrain validators ###
194+
result = asyncio.run(guard.validate("a", metadata={}))
195+
196+
assert result.validation_passed is False
197+
# The output was filtered or refrained
198+
assert result.validated_output is None
199+
assert result.reask is None
200+
201+
202+
203+
### Trigger the Reask validator ###
204+
result = asyncio.run(guard.validate("abc", metadata={}))
205+
206+
assert result.validation_passed is False
207+
# If allowed, a ReAsk would have occured
208+
assert result.reask is not None
209+
error_messages = [f.error_message for f in result.reask.fail_results]
210+
assert error_messages == ["Value must contain d", "Value must contain e"]
211+
212+
213+
### Trigger the Fix validator ###
214+
result = asyncio.run(guard.validate("abcde", metadata={}))
215+
216+
assert result.validation_passed is True
217+
# The fix values have been merged
218+
assert "f" in result.validated_output
219+
assert "g" in result.validated_output
220+
print(result.validated_output)
221+
```

docusaurus/sidebars.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const sidebars = {
6666
"concepts/streaming_fixes",
6767
],
6868
},
69-
"concepts/parallelization",
69+
"concepts/concurrency",
7070
"concepts/logs",
7171
"concepts/telemetry",
7272
"concepts/error_remediation",

0 commit comments

Comments
 (0)